Skip to main content

Type-safe LaTeX document generation with Python

Project description

PyTeX

Type-safe LaTeX document generation with Python. Build a document as a tree of typed TeX nodes and render it to a .tex file, or drop inline Python expressions into an existing .tex source and have them evaluated at render time. Requires Python 3.13+.

A TeX node is an immutable dataclass with a .rendered property. The public API mirrors LaTeX control sequences as PascalCase factories (Section, Bold, Frac, Title, ...), so a document reads like the LaTeX it produces while staying checkable by a type checker. Nodes track their package requirements, so the preamble is assembled automatically from what the body uses.

Install

To use the pytex command anywhere, install it as an isolated tool with pipx:

pipx install pytex-preprocessor

It is also available via plain pip install pytex-preprocessor.

For development, work in a virtualenv with an editable install instead:

python -m venv venv && . venv/bin/activate
pip install -e .            # add [dev] for pytest, ruff, basedpyright

External tools, each needed only for the matching feature:

  • tectonic — compile to PDF (--build). If not on PATH, the build downloads a self-contained binary into a temp folder and reuses it.
  • inkscapeSVG image conversion.
  • makeindex (from a TeX distribution, e.g. TeX Live) — resolve glossaries/acronyms.

Quick start

A .tex.py file is plain Python exposing a module-level __pytex__ that holds a TeX node:

from pytex.commands.builtin import Bold, Emph, Section, Title, MakeTitle
from pytex.model.concat import Concat
from pytex.model.document import Document
from pytex.model.math import DisplayMath, Frac

__pytex__ = Document(
    preamble=Title("PyTeX Example"),
    body=Concat(
        MakeTitle(),
        Section("Text"),
        "A paragraph with ", Bold("bold"), " and ", Emph("emphasised"), " words.",
        Section("Math"),
        DisplayMath(Concat("x = ", Frac("-b", "2a"))),
    ),
)
pytex example.tex.py          # render -> build/example.out.tex
pytex example.tex.py --build  # render + compile -> build/example.out.pdf

Bare strings are coerced to text nodes and LaTeX-escaped.

The pytex command

The input file is dispatched by extension:

Extension Handling
.py imported as a module; its __pytex__ node is rendered. Convention: name it <doc>.tex.py.
.tex wrapped in IncludeTeX; inline \iffalse{pytex(...)}\fi markers are evaluated, then rendered. Convention: <doc>.py.tex.
.md / .markdown converted to nodes and wrapped in a document according to --variant (see below). Without --variant the style is auto-detected.

Inline replacements in .tex

Any registered factory is in scope inside a marker. The \iffalse ... \fi pair is a LaTeX no-op, so the source still compiles as-is without PyTeX:

Today is \iffalse{pytex(Today())}\fi.
A fraction: $\iffalse{pytex(Frac("1", "2"))}\fi$.
Plain Python works too: $3^2 = \iffalse{pytex(3 ** 2)}\fi$.

Options

Flag Default Meaning
-o, --output <build-dir>/<input>.out.tex rendered LaTeX output path
-b, --build off compile the rendered .tex to PDF with tectonic
--build-dir DIR build directory for artifacts and tectonic output
--no-shell-escape shell-escape on disable shell-escape
-t, --tree off also print the input's TeX-node tree (tree-style) before rendering/building
-f, --force off skip the optimize + analysis pass and build even if problems are found
--variant STYLE auto-detect Markdown output style (plain, report, protocol-asta, protocol-stupa)
--config JSON none JSON object of document-class params, merged over the frontmatter

Shell-escape is on by default because inline images decode their base64 payloads at compile time. The build runs tectonic, then makeindex (for glossaries/acronyms), then reruns tectonic when an index changed.

Output is minimal and color-tagged (==>, note:, warning:, error:), following tectonic's style; on failure it points at the likely cause and the log file. Set NO_COLOR to disable color.

Pre-flight optimize + analysis

Before rendering, the builder runs two render-equivalent passes over the node tree. First Optimize tidies the tree (flatten nested Concats, drop empty nodes, turn whole-Raw LaTeX constructs into native nodes) without changing the output (it also expands inline pytex(...) markers and turns Raw comments and math — \[...\], \(...\), $...$ — into native nodes). Then pytex_analyze checks for problems that LaTeX would only surface later (or silently):

  • references (\ref, \cref, \autoref, ...) to a label that is never defined,
  • labels defined more than once,
  • \includegraphics paths that do not exist on disk.

Missing-image issues are errors and abort the build; the rest are warnings. Pass -f/--force to skip both passes and build regardless.

Inspecting the node tree

--tree prints the parsed TeX-node tree (then renders/builds as usual), useful for debugging how an input maps to nodes. Nodes that require a package are tagged with it ([+package]):

$ pytex example.tex.py --tree
Document (article)
├── ControlSequence \title
│   └── Parameter { }
│       └── Raw "PyTeX Example"
└── Concat
    ├── ControlSequence \maketitle
    ├── ControlSequence \cref [+cleveref]
    └── ...

Packages

pytex is the core; the rest are optional and build on it.

Package Provides
pytex core node model, Document, math, tables, graphics, and factories for the common LaTeX packages (biblatex, cleveref, glossaries, hyperref, listings, ...).
pytex_koma KOMA-Script classes and commands (Addchap, Minisec, KOMAoptions, ...).
pytex_tikz TikZ pictures and primitives (TikzPicture, Draw, Node, Circle, ...).
pytex_markdown Markdown -> native TeX conversion (see below).
pytex_analyze static checks over the node tree (dangling refs, duplicate labels, missing images), plus Optimize to simplify a tree render-equivalently.
pytex_hsrtreport HSRT report document class, colored callout boxes, title pages, glossary/citation helpers.
pytex_protocol STUPA/AStA meeting minutes from Markdown, built on pytex_hsrtreport.

Markdown

pytex_markdown converts Markdown to native TeX nodes (via marko):

from pytex_markdown import Markdown, IncludeMarkdown

body = Markdown("# Title\n\nText with **bold**, `code`, [a link](https://x).")
body = IncludeMarkdown("notes.md", base_level=-1)   # base_level=-1: # -> \chapter

Headings, emphasis, inline/fenced code, lists, links, images, block quotes and thematic breaks map to the standard pytex library; text is LaTeX-escaped. GitHub-style callouts become HSRT colored boxes (so the module depends on pytex_hsrtreport):

> [!NOTE]      -> InfoBox        > [!IMPORTANT] -> ImportantBox
> [!TIP]       -> SuccessBox     > [!WARNING]   -> WarningBox

Both factories are registered, so they work in \iffalse{pytex(...)}\fi replacements in .tex sources too.

Output variants

When the pytex command renders a .md file it wraps the converted nodes in a document chosen by --variant:

Variant Document
plain a bare Document (default class article); # -> \section.
report an HSRT report with title page and table of contents; # -> \chapter.
protocol-asta an AStA meeting protocol (HSRT report, AStA logos).
protocol-stupa a StuPa meeting protocol (HSRT report, StuPa logos).

Without --variant, protocol frontmatter (gremium: or typ: protokoll) picks a protocol style and everything else falls back to plain.

Document-class parameters come from the YAML frontmatter and from --config (a JSON object that overrides the frontmatter), e.g.:

pytex notes.md --variant plain --config '{"documentclass": "scrartcl", "classoptions": ["11pt", "twocolumn"]}'

classoptions accepts a list ("twocolumn", "DIV=12") or a {key: value} object. For styles with a title page (report), the title is taken from title:/--config if given, otherwise from the first # heading (which is then not also rendered as a chapter).

Converting LaTeX to PyTeX

pytex-tex2py turns an existing .tex file into an equivalent .tex.py source. It reads the file, runs Optimize over it (expanding inline pytex(...) markers and recognising comments and math), and serialises the result to Python that rebuilds the same tree:

pytex-tex2py paper.tex            # -> paper.tex.py
pytex-tex2py paper.tex -o out.py

Rendering the generated .tex.py reproduces the original output byte-for-byte; nodes the serialiser does not special-case fall back to a literal Raw, so the conversion always round-trips.

Examples

See examples/ for one minimal input per kind (.tex.py, .py.tex, .md, mixed, and a full HSRT report). Run from the repository root so relative paths resolve:

pytex examples/document.tex.py --build
pytex examples/replacements.py.tex --build
pytex examples/notes.md --build

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pytex_preprocessor-0.3.0.tar.gz (1.1 MB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pytex_preprocessor-0.3.0-py3-none-any.whl (1.2 MB view details)

Uploaded Python 3

File details

Details for the file pytex_preprocessor-0.3.0.tar.gz.

File metadata

  • Download URL: pytex_preprocessor-0.3.0.tar.gz
  • Upload date:
  • Size: 1.1 MB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for pytex_preprocessor-0.3.0.tar.gz
Algorithm Hash digest
SHA256 7dd81d93cdbf01450c8770ba87f443fa7e5059b7cf71d1caed3500ffd064122e
MD5 056edaa9c4534a1352a4b38eb774e323
BLAKE2b-256 0d09fd380b118b9df08681ed4d2dd0abec937ba8436c0505cef7c6ce598f79eb

See more details on using hashes here.

Provenance

The following attestation bundles were made for pytex_preprocessor-0.3.0.tar.gz:

Publisher: release.yml on frederikbeimgraben/PyTeX-Preprocessor

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file pytex_preprocessor-0.3.0-py3-none-any.whl.

File metadata

File hashes

Hashes for pytex_preprocessor-0.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 3b92e35243d55b45857cac9be0018eb6d45cf45224925665c08cdc3763f25187
MD5 0473d522c1c7d2880ab241ee97ebd44c
BLAKE2b-256 b7193097a9494a83c242010990aa6571000d3cf844ffb1b0af1cb65d9fb5b6e4

See more details on using hashes here.

Provenance

The following attestation bundles were made for pytex_preprocessor-0.3.0-py3-none-any.whl:

Publisher: release.yml on frederikbeimgraben/PyTeX-Preprocessor

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page