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
Prebuilt binary
Each release attaches standalone pytex binaries (Linux/macOS/Windows) — no
Python or pip needed. Download one from the
Releases
page, make it executable, and run it. The binary bundles its own interpreter
plus common data packages (numpy, pandas, openpyxl/calamine for spreadsheets,
Pillow, PyYAML), so documents can import those without installing anything;
see packaging/. It is built on Python 3.14, so documents may use
tex(t"...") even on machines without 3.14. (--build still needs tectonic,
which pytex downloads on first use.)
From PyPI
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 onPATH, the build downloads a self-contained binary into a temp folder and reuses it.inkscape—SVGimage 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.
Template strings (Python 3.14+)
On Python 3.14, pytex.tex accepts a PEP 750
template string and builds a TeX tree from it. Static parts are literal LaTeX;
interpolations are LaTeX-escaped when they are plain values and spliced as-is
when they are TeX nodes (nested template strings and lists are handled too):
from pytex import tex
name = "Q&A: 50%"
body = tex(t"{Bold('Heading')} — {name}") # node spliced; name -> "Q\&A: 50\%"
tex is only exported on 3.14+; the rest of the library runs on 3.13.
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,
\includegraphicspaths 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
License
GNU General Public License v3.0 or later (GPL-3.0-or-later). See
LICENSE.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pytex_preprocessor-0.4.7.tar.gz.
File metadata
- Download URL: pytex_preprocessor-0.4.7.tar.gz
- Upload date:
- Size: 1.2 MB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
bc29075421f7fd6119502a3779fdc90a132320f5c8e8f5b6e4f6429842c940ed
|
|
| MD5 |
19648bd3cb717bca894c42e0f82be1e7
|
|
| BLAKE2b-256 |
8e5a30812ac96ef5bfb7106cbb87980469e6b837edf48ac3f6d3ce8480f172bb
|
Provenance
The following attestation bundles were made for pytex_preprocessor-0.4.7.tar.gz:
Publisher:
release.yml on frederikbeimgraben/PyTeX-Preprocessor
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pytex_preprocessor-0.4.7.tar.gz -
Subject digest:
bc29075421f7fd6119502a3779fdc90a132320f5c8e8f5b6e4f6429842c940ed - Sigstore transparency entry: 1713020193
- Sigstore integration time:
-
Permalink:
frederikbeimgraben/PyTeX-Preprocessor@226c5b3035e7c6d4de7d78ab4b1b7b0bf8eb8016 -
Branch / Tag:
refs/tags/v0.4.7 - Owner: https://github.com/frederikbeimgraben
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@226c5b3035e7c6d4de7d78ab4b1b7b0bf8eb8016 -
Trigger Event:
push
-
Statement type:
File details
Details for the file pytex_preprocessor-0.4.7-py3-none-any.whl.
File metadata
- Download URL: pytex_preprocessor-0.4.7-py3-none-any.whl
- Upload date:
- Size: 1.2 MB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
22ee38f2030564e0e08fe870dc53aee2048c43a0a1193ab47f00494a777de3f5
|
|
| MD5 |
8ee5a9e43aa4709f4d200857b80bccb3
|
|
| BLAKE2b-256 |
f00b166ae045d9b42f1d5f18b9d37cff1d48aa0f13ecafc17c667d757488e66b
|
Provenance
The following attestation bundles were made for pytex_preprocessor-0.4.7-py3-none-any.whl:
Publisher:
release.yml on frederikbeimgraben/PyTeX-Preprocessor
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pytex_preprocessor-0.4.7-py3-none-any.whl -
Subject digest:
22ee38f2030564e0e08fe870dc53aee2048c43a0a1193ab47f00494a777de3f5 - Sigstore transparency entry: 1713020247
- Sigstore integration time:
-
Permalink:
frederikbeimgraben/PyTeX-Preprocessor@226c5b3035e7c6d4de7d78ab4b1b7b0bf8eb8016 -
Branch / Tag:
refs/tags/v0.4.7 - Owner: https://github.com/frederikbeimgraben
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@226c5b3035e7c6d4de7d78ab4b1b7b0bf8eb8016 -
Trigger Event:
push
-
Statement type: