Reactive Python execution engine for Typst documents
Project description
typst_pyexec
typst_pyexec is a reactive Python execution engine for Typst documents.
It executes Python fences inside .typ files, captures outputs (stdout, figures, tables), and injects rendered Typst markup into an intermediate file (*.typst_pyexec.typ) that can be compiled directly.
Requirements
- Python >= 3.10
- Typst compiler for
buildandwatch(default commandtypst) - Optional: tinymist for faster live preview (
tinymist preview)
Core Capabilities
- Reactive dependency graph based on Python AST analysis
- Incremental execution with source-hash cache
- Persistent Jupyter kernel between builds with cold-kernel hydration
- Matplotlib figure export to SVG with PNG fallback
- Subfigure reconstruction via
figure(grid(...)) - DataFrame HTML rendering to Typst
#table(...) - Watch mode with automatic rebuilds and live preview
Installation
pip install typst_pyexec
For development:
uv sync --extra dev
CLI Usage
Build once:
typst_pyexec build document.typ
Watch and rebuild on save:
typst_pyexec watch document.typ
Watch with explicit preview backend:
typst_pyexec watch document.typ --preview-engine auto
Clean local state directory:
typst_pyexec clean
Common options:
--output-dir <dir>: write intermediate and state outputs to a custom folder (default: source file directory)--no-cache: disable cache reads and writes, and execute all cells--jobs <n>: reserved for future multi-kernel scheduling (-1default)--compiler <cmd>: Typst compiler binary name or path (defaulttypst)watch --preview-engine <auto|tinymist|typst|none>: select live preview backend
Watch mode behavior
- Watch mode regenerates
*.typst_pyexec.typon each save and delegates rendering to a live preview process. --preview-engine autotriestinymist previewfirst (iftinymistis on PATH), then falls back totypst watch.--preview-engine tinymistpreferstinymist preview; iftinymistis unavailable it falls back totypst watch.--preview-engine typstalways runstypst watch.--preview-engine nonedisables live preview and only refreshes the intermediate file.
Block Options (%|)
Place options at the top of a Python fence:
```python
%| echo: false
%| raw: true
%| figure: true
%| plt-lines.linewidth: 0.8
%| plt-axes.linewidth: 0.6
print("hello")
```
Supported options:
execute(defaulttrue): skip execution whenfalserefresh(defaultfalse): force this cell to run every buildecho(defaulttrue): show or hide source coderaw(defaulttrue): show or hide textual runtime output (stdout, tracebacks,text/plainbundles)figure(defaulttrue): show or hide rendered figurescaption: explicit figure caption overridelabel: Typst label for cross-referenceskeep-subplots(defaultfalse): preserve multi-axis plot as one imagesubfigure-caption-position(toporbottom): position subfigure captions for subplot gridsimg-*: passthrough kwargs for Typstimage(...)fig-*: passthrough kwargs for the outer/global Typstfigure(...)subfig-*: passthrough kwargs for each generated Typst subfigure (kind: "subfigure")grid-*: passthrough kwargs for Typstgrid(...)grid-columns: explicitcolumnsvalue for the grid (overrides inferred default)plt-*: per-cell matplotlibrcParamsoverrides (key afterplt-maps to rcParam name)
Example for subplot grids with captions on top and separate outer/inner styling:
```python
%| keep-subplots: false
%| subfigure-caption-position: top
%| fig-supplement: "Figure"
%| fig-placement: top
%| subfig-supplement: "Figure"
%| subfig-stroke: rgb("#888")
```
In this configuration, fig-* applies only to the outer/global figure, and subfig-* applies to each child subfigure.
Boolean values are case-insensitive and accept: true/false, yes/no, on/off, 1/0.
plt-* values are parsed as JSON first, then Python literals. If parsing fails, the raw string is used.
Default plt-* values applied to every executed cell:
lines.linewidth: 0.8axes.linewidth: 0.6grid.linewidth: 0.2axes.grid: truelines.markersize: 4(scatter points are about 2x smaller in area than matplotlib default)
plt-* examples:
%| plt-lines.linewidth: 0.8%| plt-axes.linewidth: 0.6%| plt-grid.linewidth: 0.4%| plt-axes.grid: true
Precedence order for plot style values:
- Built-in defaults (values above)
%| plt-*metadata options- Any
matplotlib.rcParams[...] = ...orplt.rcParams.update(...)in Python code
This means Python code always wins over %| plt-*, and %| plt-* wins over defaults.
Behavior notes:
cell_idis internal and generated automatically.- Python fences can be indented to fit paragraph or layout context; shared left padding is removed before execution.
- In generated
.typst_pyexec.typ, that original block padding is preserved for both rendered source and rendered outputs. - Option-only edits (including
plt-*) do not invalidate cache because cache keys are based on Python source. - To re-run on option updates, set
refresh: true.
Figure and Caption Behavior
- Matplotlib figures are exported only when explicitly shown (
plt.show()orfig.show()). - For single-axis plots, title or suptitle text is promoted into the Typst caption when
captionis not set. - For subplot grids, each axis title becomes a child caption; suptitle becomes the outer caption.
- Use
%| subfigure-caption-position: topto place subplot captions above each subplot image. - In subplot grids,
fig-*affects only the outer/global figure, whilesubfig-*affects each child subfigure. - With
keep-subplots: true, multi-axis figures are kept as one image and subplot titles remain inside the image (the first subplot title is not promoted to caption). - Title and suptitle text are removed from exported images only when they are promoted to captions.
- If SVG export fails and PNG is emitted, renderer auto-resolves PNG paths in final Typst output.
How It Works
- Parse Python fences from Typst source
- Build def/use DAG from AST
- Compute changed cells from source-hash cache
- Execute required cells in topological groups
- Capture stdout, display bundles, and figure artifacts
- Render Typst fragments per cell
- Inject into
document.typst_pyexec.typ - Build mode: compile with Typst compiler
- Watch mode: refresh intermediate file and stream live preview via
tinymist previewortypst watch
Caching and Execution Model
- Cache keys are SHA-256 hashes of the Python source for each cell.
- Cache entries are stored under
.typst_pyexec/cache. --no-cachedisables cache reads and writes and forces all executable cells to run.refresh: trueforces a cell to execute every build but does not cascade to dependents.- When a kernel is cold or reconnected, typst_pyexec replays prerequisite cells to rebuild namespace state.
Local State
typst_pyexec writes runtime state into .typst_pyexec/ inside the output directory:
kernel_connection.json: reconnect data for persistent kernelcache/: JSON entries keyed by SHA-256 of cell sourcefigures/: SVG/PNG artifactsnotebook.ipynb: synchronized notebook in normal execution mode (original user cells)notebook_export.ipynb: synchronized notebook in export mode (figure capture preamble/postamble included)
Notebook Modes
Two notebooks are intentionally generated:
notebook.ipynbpreserves the authored Python cells and attached outputs exactly as written (minimal overhead).notebook_export.ipynbwraps each cell with figure-export helpers that invoke optimized functions.
Both notebooks include a setup cell that initializes the environment:
- Normal mode: sets working directory for relative path consistency
- Export mode: sets working directory and imports matplotlib plus
save_figures_and_metadata, enabling the export notebook to execute standalone
Development Quality Gates
Run checks locally:
uv run ruff check .
uv run black --check typst_pyexec tests
uv run mypy typst_pyexec
uv run pytest
CI/CD (GitHub Actions)
CI: matrix on Ubuntu and Windows, Python 3.10-3.12, with lint + format + type-check + tests + coverage artifactRelease: tag-driven (v*.*.*) build and publish workflow for PyPI using trusted publishing
Workflows are in:
.github/workflows/ci.yml.github/workflows/release.yml
Publish to PyPI
typst_pyexec is already configured for trusted publishing through GitHub Actions.
Release steps:
- Bump version in
pyproject.tomlandtypst_pyexec/__init__.py. - Commit and push to main.
- Create and push a version tag:
git tag v0.1.1
git push origin v0.1.1
- GitHub Actions runs
.github/workflows/release.ymland publishes to PyPI.
Pre-tag checklist:
python -m pytest -qis green.python -m buildsucceeds.python -m twine check dist/*passes.- Version matches in
pyproject.tomlandtypst_pyexec/__init__.py. - Git tag matches that version (
vX.Y.Z).
Local preflight checks before tagging:
python -m build
python -m twine check dist/*
If you prefer token-based manual publishing:
python -m twine upload dist/*
Use in Other Repositories
After publishing, install as a normal dependency.
With pip:
pip install typst_pyexec
With uv (project dependency):
uv add typst_pyexec
With uvx (run CLI without adding dependency):
uvx typst_pyexec build document.typ
License
MIT
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
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 typst_pyexec-0.0.6.tar.gz.
File metadata
- Download URL: typst_pyexec-0.0.6.tar.gz
- Upload date:
- Size: 192.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c00bf078389daaed9bc5586a1bfac4e51a1b0773c17c1c4a875292e562b99357
|
|
| MD5 |
699b1fd00cb86cd45a7baa674bd7b3ed
|
|
| BLAKE2b-256 |
5b78a9b897d8777475a6c85d262c519fc07b6c1bb8ea1d0429540e1d872ba4bc
|
Provenance
The following attestation bundles were made for typst_pyexec-0.0.6.tar.gz:
Publisher:
release.yml on Arkanthara/typst_pyexec
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
typst_pyexec-0.0.6.tar.gz -
Subject digest:
c00bf078389daaed9bc5586a1bfac4e51a1b0773c17c1c4a875292e562b99357 - Sigstore transparency entry: 1234791304
- Sigstore integration time:
-
Permalink:
Arkanthara/typst_pyexec@b8b04907f16f53048d84ff40ef17b0b81432347f -
Branch / Tag:
refs/tags/v0.0.6 - Owner: https://github.com/Arkanthara
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@b8b04907f16f53048d84ff40ef17b0b81432347f -
Trigger Event:
push
-
Statement type:
File details
Details for the file typst_pyexec-0.0.6-py3-none-any.whl.
File metadata
- Download URL: typst_pyexec-0.0.6-py3-none-any.whl
- Upload date:
- Size: 41.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d2c5ac7d71aeffb27f4832f38138795b5cfca0afa9653a2a4ca1f5b38f2bcdf0
|
|
| MD5 |
b2d829b43086640e1914167328149d50
|
|
| BLAKE2b-256 |
265218429a0c89368eb207712224535d43c55d0b8bfb2e4f3b8f754673d4773f
|
Provenance
The following attestation bundles were made for typst_pyexec-0.0.6-py3-none-any.whl:
Publisher:
release.yml on Arkanthara/typst_pyexec
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
typst_pyexec-0.0.6-py3-none-any.whl -
Subject digest:
d2c5ac7d71aeffb27f4832f38138795b5cfca0afa9653a2a4ca1f5b38f2bcdf0 - Sigstore transparency entry: 1234791349
- Sigstore integration time:
-
Permalink:
Arkanthara/typst_pyexec@b8b04907f16f53048d84ff40ef17b0b81432347f -
Branch / Tag:
refs/tags/v0.0.6 - Owner: https://github.com/Arkanthara
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@b8b04907f16f53048d84ff40ef17b0b81432347f -
Trigger Event:
push
-
Statement type: