Reports and documents#

Recipes for getting calculations into a deliverable: a PDF, a Word file, a web page or a Quarto report, with equation numbers, a title block and your colors.

Write a calculation straight to a file#

Asked in #177, #138, #128 and #203.

You don’t need Jupyter or a notebook export. HandCalcs renders a .py file (or a string) with any renderer, and destination= writes the result to a file:

import pathlib
from handcalcs import HandCalcs
from handcalcs.renderers import LatexRenderer, HTMLRenderer, MarkdownRenderer

calc = pathlib.Path("calcs/beam.py")
HandCalcs(LatexRenderer())(calc, destination="report/beam.tex")
HandCalcs(HTMLRenderer())(calc, destination="report/beam.html")
HandCalcs(MarkdownRenderer())(calc, destination="report/beam.md")

The .tex file is a complete document that compiles with pdflatex, and the .html file is a complete page that needs no MathJax or other JavaScript. See Writing a file.

A PDF through LaTeX#

For a PDF of one calculation, write it with destination="beam.tex" (above) and run pdflatex beam.tex.

To put calculations into a larger report, render each one without a destination, save the fragment, and \input it. The fragments need amsmath, plus amssymb and xcolor for --check marks:

pathlib.Path("report/beam.tex").write_text(HandCalcs(LatexRenderer())(calc))
\documentclass{article}
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{xcolor}
\begin{document}
\input{beam.tex}
\end{document}

See LaTeX renderer.

A Word document#

Asked in #222 and #228.

The Word renderer writes a .docx file with native, editable Word equations:

import pathlib
from handcalcs import HandCalcs
from handcalcs.renderers import WordRenderer

HandCalcs(WordRenderer())(pathlib.Path("calcs/beam.py"), destination="beam.docx")

Install it with pip install "handcalcs[word]". It can add to an existing document (document=) or start from your company template (template="company.docx"). See Word renderer.

A Quarto or MyST report#

The Markdown output works in Quarto (.qmd) and MyST documents as it is. Render a calculation in a code cell with %%render, or write the Markdown to a file and include it. See Where it works.

Share a notebook without the code#

To share a notebook without the code cells, export it with nbconvert’s --no-input option:

jupyter nbconvert --to html --no-input calcs.ipynb
jupyter nbconvert --to pdf --no-input calcs.ipynb

pip install "handcalcs[exporters]" also adds “no input” export options to Jupyter’s menus. See Exporting a notebook.

Start a new page#

Asked in #233.

# hc: --pagebreak starts a new page in printed and exported output: \newpage in LaTeX, a CSS page break in HTML and Markdown, a real page break in Word and a print page break in Excel. On its own line it breaks before the next line; at the end of a line it breaks after it:

## Loads
w = 5            # kN/m
L = 6            # m
# hc: --pagebreak
## Design
M = w * L**2 / 8
V = w * L / 2    # hc: --pagebreak

See Page breaks.

Number the equations#

Asked in #218 and #200.

A calc_line:post rule can append a number to each calculation. This one reads two context fields: eq_numbers = True numbers every calculated line in order, and label = "..." at the end of a line gives that line its own label:

"""Equation numbers on calculation lines (#218)."""
from handcalcs.renderers import LatexRenderer, MarkdownRenderer


def number_equations(renderer, rendered, node, base_context):
    """
    '# hc: eq_numbers = True' numbers every calculated line (1), (2), ...
    'x = ...  # hc: label = "4.2"' gives one line its own number.
    """
    commands = getattr(node.comment, "commands", None) or {}
    label = commands.get("label")
    # A parameter (target = value) has one assign symbol; a calculation has two or more.
    is_param = isinstance(rendered, list) and rendered.count(base_context.current.assign) < 2
    if label is None and getattr(base_context.current, "eq_numbers", False) and not is_param:
        count = getattr(base_context.global_context, "eq_count", 0) + 1
        base_context.global_context.eq_count = count
        label = count
    if label is None:
        return rendered
    tag = f"\\qquad ({label})"
    if isinstance(rendered, list):
        # Tag the result, which is right after the last assign symbol (a comment may follow it).
        assign = base_context.current.assign
        result = max(i for i, part in enumerate(rendered) if part == assign) + 1
        rendered[result] = rendered[result] + tag
        return rendered
    return rendered + tag  # a multi-line (# hc: -m) calc is one string


class NumberedMarkdownRenderer(MarkdownRenderer):
    name = "numbered_markdown"


class NumberedLatexRenderer(LatexRenderer):
    name = "numbered_latex"


NumberedMarkdownRenderer.register("calc_line:post")(number_equations)
NumberedLatexRenderer.register("calc_line:post")(number_equations)
# hc: eq_numbers = True
w = 5                     # kN/m
L = 6                     # m
M = w * L**2 / 8          # kN m
V = w * L / 2             # kN
R_A = V                   # hc: label = "B.1"
Output (NumberedMarkdownRenderer)
\[\begin{aligned} w &= 5 \quad\text{(kN/m)} \\ L &= 6 \quad\text{(m)} \\ M &= \frac{w \cdot L^{2}}{8} = \frac{5 \cdot 6^{2}}{8} = 22.5\qquad (1) \\ &\quad\text{(kN m)} \\ V &= \frac{w \cdot L}{2} = \frac{5 \cdot 6}{2} = 15\qquad (2) \\ &\quad\text{(kN)} \\ R_{A} &= V = 15\qquad (B.1) \end{aligned}\]
$$\begin{aligned} w &= 5 \quad\text{(kN/m)} \\ L &= 6 \quad\text{(m)} \\ M &= \frac{w \cdot L^{2}}{8} = \frac{5 \cdot 6^{2}}{8} = 22.5\qquad (1) \\ &\quad\text{(kN m)} \\ V &= \frac{w \cdot L}{2} = \frac{5 \cdot 6}{2} = 15\qquad (2) \\ &\quad\text{(kN)} \\ R_{A} &= V = 15\qquad (B.1) \end{aligned}$$

The number is placed in the math as \qquad (n), so it works in Jupyter, KaTeX and LaTeX alike. For LaTeX cross-references, emit \tag{...}\label{...} instead and use an align environment in your own subclass.

A title block on every calculation#

Asked in #137.

Override complete(), which assembles the final output, and add a header built from context fields. Set the fields once per session with handcalcs.set_option("project", "Footbridge"), or per calculation with a comment command:

"""A title block at the top of every calculation (#137)."""
from handcalcs.renderers import LatexRenderer, MarkdownRenderer

FIELDS = (("project", "Project"), ("calc_by", "By"), ("checked_by", "Checked"), ("date", "Date"))


def title_rows(base_context):
    """(label, value) for each title field that is set, from config or '# hc:' commands."""
    context = base_context.global_context
    return [(label, getattr(context, name)) for name, label in FIELDS if hasattr(context, name)]


class TitleMarkdownRenderer(MarkdownRenderer):
    name = "title_markdown"

    def complete(self, tree, base_context):
        body = super().complete(tree, base_context)
        rows = title_rows(base_context)
        if not rows:
            return body
        table = "| " + " | ".join(label for label, _ in rows) + " |\n"
        table += "|" + "---|" * len(rows) + "\n"
        table += "| " + " | ".join(str(value) for _, value in rows) + " |\n\n"
        return table + body


class TitleLatexRenderer(LatexRenderer):
    name = "title_latex"

    def complete(self, tree, base_context):
        body = super().complete(tree, base_context)
        rows = title_rows(base_context)
        if not rows:
            return body
        cells = " & ".join(f"\\textbf{{{label}:}} {value}" for label, value in rows)
        header = (
            "\\noindent\\begin{tabular}{" + "l" * len(rows) + "}\n"
            + cells + "\n\\end{tabular}\n\n\\noindent\\hrulefill\n\n"
        )
        return header + body
# hc: project = "Footbridge"; calc_by = "CF"; date = "2026-10-20"
w = 5                     # kN/m
L = 6                     # m
M = w * L**2 / 8          # kN m
Output (TitleMarkdownRenderer)

Project

By

Date

Footbridge

CF

2026-10-20

\[\begin{aligned} w &= 5 \quad\text{(kN/m)} \\ L &= 6 \quad\text{(m)} \\ M &= \frac{w \cdot L^{2}}{8} = \frac{5 \cdot 6^{2}}{8} = 22.5 \\ &\quad\text{(kN m)} \end{aligned}\]
| Project | By | Date |
|---|---|---|
| Footbridge | CF | 2026-10-20 |

$$\begin{aligned} w &= 5 \quad\text{(kN/m)} \\ L &= 6 \quad\text{(m)} \\ M &= \frac{w \cdot L^{2}}{8} = \frac{5 \cdot 6^{2}}{8} = 22.5 \\ &\quad\text{(kN m)} \end{aligned}$$

TitleLatexRenderer writes the same block as a tabular for LaTeX output.

Left-aligned or centred equations#

Asked in #201, #186, #133, #116 and #91.

The LaTeX renderer sets its math flush left. To centre it instead, override the math delimiters:

"""A LatexRenderer that sets each calculation group as a centred display."""
from handcalcs.renderers import LatexRenderer


class DisplayLatexRenderer(LatexRenderer):
    name = "display_latex"

    MATH_OPEN = "\\[\n\\begin{aligned}"
    MATH_CLOSE = "\\end{aligned}\n\\]"
    MATH_SKIP = ""
    OPERATOR_SYMBOLS = LatexRenderer.OPERATOR_SYMBOLS | {"mult_op": "\\times"}

The Markdown renderer’s $$...$$ blocks are placed by your notebook or site; Jupyter centres them. The HTML renderer is always flush left.

Colors#

Asked in #293.

Wrap the whole output#

Override complete(), call the parent’s, and add to the result. For example, a framed LaTeX box:

class FramedLatexRenderer(LatexRenderer):
    def complete(self, tree, base_context):
        body = super().complete(tree, base_context)
        return "\\begin{framed}\n" + body + "\\end{framed}\n"

Next: Apps and workflows.