Execute code blocks in markdown files and insert the results.
Use cases:
- Keep documentation examples up-to-date automatically
- Validate code snippets in docs actually work
- Generate diagrams and charts from code in markdown
- Literate programming with executable documentation
echo "cwd: $(pwd)"cwd: /work
a = "hello world"
print(a)hello world
Sessions preserve state between code blocks:
print(a, "again")hello world again
console.log("Hello from Node.js");
console.log(`Node version: ${process.version}`);Hello from Node.js
Node version: v22.21.1
import matplotlib.pyplot as plt
import numpy as np
plt.style.use('dark_background')
x = np.linspace(0, 4 * np.pi, 200)
plt.figure(figsize=(8, 4))
plt.plot(x, np.sin(x), label='sin(x)', linewidth=2)
plt.plot(x, np.cos(x), label='cos(x)', linewidth=2)
plt.xlabel('x')
plt.ylabel('y')
plt.legend()
plt.grid(alpha=0.3)
plt.savefig('{output}', transparent=True)SQLite's diagram language:
color = white
fill = none
linewid = 0.4in
# Input file
In: file "README.md" fit
arrow
# Processing
Parse: box "Parse" rad 5px fit
arrow
Exec: box "Execute" rad 5px fit
# Fan out to languages
arrow from Exec.e right 0.3in then up 0.4in then right 0.3in
Sh: oval "Shell" fit
arrow from Exec.e right 0.3in then right 0.3in
Node: oval "Node" fit
arrow from Exec.e right 0.3in then down 0.4in then right 0.3in
Py: oval "Python" fit
# Merge back
X: dot at (Py.e.x + 0.3in, Node.e.y) invisible
line from Sh.e right until even with X then down to X
line from Node.e to X
line from Py.e right until even with X then up to X
Out: file "README.md" fit with .w at (X.x + 0.3in, X.y)
arrow from X to Out.wVector graphics:
import graph;
import stats;
size(400,200,IgnoreAspect);
defaultpen(white);
int n=10000;
real[] a=new real[n];
for(int i=0; i < n; ++i) a[i]=Gaussrand();
draw(graph(Gaussian,min(a),max(a)),orange);
int N=bins(a);
histogram(a,min(a),max(a),N,normalize=true,low=0,rgb(0.4,0.6,0.8),rgb(0.2,0.4,0.6),bars=true);
xaxis("$x$",BottomTop,LeftTicks,p=white);
yaxis("$dP/dx$",LeftRight,RightTicks(trailingzero),p=white);A -> B -> C
A -> Ccube([10, 10, 10]);
sphere(r=7);ASCII art diagrams:
1 + 1/2 + sum(i,0,10)
10
___
1 ╲
1 + ─ + ╱ i
2 ‾‾‾
0
A -> B -> C
A -> C
┌───┐
│A │
└┬─┬┘
│┌▽┐
││B│
│└┬┘
┌▽─▽┐
│C │
└───┘
# Run directly from GitHub
nix run github:leshy/md-babel-py -- run README.md --stdout
# Or clone and run locally
nix run . -- run README.md --stdout# Pull from Docker Hub
docker run -v $(pwd):/work lesh/md-babel-py:main run /work/README.md --stdout
# Or build locally via Nix
nix build .#docker # builds tarball to ./result
docker load < result # loads image from tarball
docker run -v $(pwd):/work md-babel-py:latest run /work/file.md --stdoutpipx install md-babel-py
# or: uv pip install md-babel-py
md-babel-py run README.md --stdoutIf not using nix or docker, evaluators require system dependencies:
| Language | System packages |
|---|---|
| python | python3 |
| node | nodejs |
| dot | graphviz |
| asymptote | asymptote, texlive, dvisvgm |
| pikchr | pikchr |
| openscad | openscad, xvfb, imagemagick |
| diagon | diagon |
# Arch Linux
sudo pacman -S python nodejs graphviz asymptote texlive-basic openscad xorg-server-xvfb imagemagick
# Debian/Ubuntu
sudo apt-get install python3 nodejs graphviz asymptote texlive xvfb imagemagick openscadNote: pikchr and diagon may need to be built from source. Use Docker or Nix for full evaluator support.
# Edit file in-place
md-babel-py run document.md
# Output to separate file
md-babel-py run document.md --output result.md
# Print to stdout
md-babel-py run document.md --stdout
# Only run specific languages
md-babel-py run document.md --lang python,sh
# Only run blocks matching some text (like pytest's -k)
md-babel-py run document.md -k "session=plot"
# Re-run automatically whenever the file changes
md-babel-py run document.md --watch
# Dry run - show what would execute
md-babel-py run document.md --dry-run
# Longer limit for each isolated subprocess (default 60s; useful for CI or large downloads)
md-babel-py run document.md --execution-timeout 120Isolated evaluators (non-session blocks) run each snippet in a subprocess and stop it after this many seconds. Session-based blocks use separate timeouts inside the session.
-k TEXT runs only the blocks whose text contains TEXT, case-insensitively.
It searches the code, the language, the session name (as plot or
session=plot), and params (as output=diagram.svg).
Selection applies to execution, so a session block picked out on its own will not have the state its earlier blocks would have built.
--watch runs the file (or directory, with --recursive) and then keeps
running it whenever it changes, until interrupted:
md-babel-py run docs/ --recursive --watch
md-babel-py run document.md --watch --watch-interval 0.5 # default is 1sChanges are detected by hashing file contents, not by mtime or inode, so a
save-via-rename from an editor, a cp -p, and a git checkout all register.
md-babel's own rewrite of the file it just ran does not count as a change, so
watching does not loop. A file that is still being written is left alone until
its contents stop changing.
When editing a file in place, each block's result is written as soon as that block finishes, so a long document fills in as it runs instead of all at once at the end.
Every write re-reads the file first and applies the result to the document as it
stands, so edits made while the run is in progress are kept. If a block itself is
edited while it is running, the result it was computing is dropped rather than
written over the new code. Under --watch, a file edited mid-run is processed
again instead of having that edit absorbed.
--stdout and --output are unaffected: they produce one document at the end.
Block results are cached by default (in $XDG_CACHE_HOME/md-babel); pass
--no-cache to re-execute everything.
An isolated block is keyed by its own code and evaluator config. A session block is keyed by the chain of blocks before it in that session, so a session whose blocks are all unchanged is served entirely from cache without ever starting the interpreter. Editing a block invalidates that block and every block after it in the same session; other sessions in the file are unaffected.
Because a block that misses needs the REPL state its predecessors built, any session containing a miss is re-executed in full.
```python session=main
x = 42
```| Flag | Description |
|---|---|
session=NAME |
Share state with other blocks using the same session name |
output=PATH |
Write output to file (for images/diagrams) |
expected-error |
Expect this block to fail; test fails if it succeeds |
skip |
Don't execute this block |
no-result |
Execute but don't insert result block |
fold |
Wrap code in collapsible <details> (uses language as summary) |
fold="TEXT" |
Wrap code in collapsible <details> with custom summary |
Results are inserted after the code block. Use the fold flag to automatically wrap code in a collapsible <details> element:
```python fold
print("hello")
```This produces:
<details><summary>Python</summary>
```python fold
print("hello")
```
</details>
```results
hello
```For manual control, if a code block is inside a <details> tag, the result is placed after </details>:
<details>
<summary>diagram source</summary>
```pikchr output=diagram.svg
box "Hello"
```
</details>
Any key=value pair becomes a parameter for the evaluator command:
```diagon mode=GraphDAG
A -> B
```With config "defaultArguments": ["{mode}"], the {mode} placeholder is replaced with GraphDAG.
- uses: leshy/md-babel-py@main
with:
files: 'README.md docs/*.md'| Input | Description | Default |
|---|---|---|
files |
Markdown files to process (glob patterns) | required |
args |
Additional arguments | '' |
fail-on-change |
Fail if files were modified (CI check) | false |
Example with auto-commit:
- uses: leshy/md-babel-py@main
with:
files: '*.md docs/**/*.md'
- uses: stefanzweifel/git-auto-commit-action@v5
with:
commit_message: 'Update markdown code block results'Create config.json in your project or ~/.config/md-babel/config.json:
{
"evaluators": {
"codeBlock": {
"python": {
"path": "/usr/bin/env",
"defaultArguments": ["python3"],
"session": {
"command": ["python3", "-i"],
"prompts": [">>> ", "... "]
}
}
}
}
}For tools that use input/output files:
{
"openscad": {
"path": "xvfb-run",
"defaultArguments": ["-a", "openscad", "-o", "{output_file}", "{input_file}"],
"inputExtension": ".scad"
}
}Two .envrc files are provided:
| File | Description |
|---|---|
.envrc.nix |
Nix flake devShell (all evaluators + dev tools) |
.envrc.venv |
Python venv only |
ln -s .envrc.nix .envrc
direnv allownix develop
# Provides: md-babel-py, pytest, mypy, ruff, and all evaluatorspip install -e ".[dev]"
pytest tests/ -v
mypy md_babel_py/
ruff check md_babel_py/| Package | Description |
|---|---|
default |
Full package with all evaluators in PATH |
minimal |
Just Python package, no bundled evaluators |
docker |
Docker image tarball |
MIT

