A lightweight, zero-dependency way to turn Markdown into a runnable notebook.
Execute Python or Bash code blocks and write results anywhere in the same file, no hidden state, no UI, just plain text.
The following timestamp auto-updates when the notebook is run.
The current time is: April 19, 2026 at 08:29 AM
from datetime import datetime
now = datetime.now()
print(f"**{now:%B %d, %Y at %I:%M %p}**")Run:
mdexec README.md
and the timestamp updates in-place.
- Add
execto any code block you want executed - Capture output with
output-id=... - Reference locations in Markdown using
<!-- id:... --> ... <!-- /id:... -->orid=...in another code block mdexecrewrites only the parts that change
No hidden metadata. Just Markdown.
Install directly from GitHub:
pip install git+https://github.com/dthigpen/mdexec.git
- Create a Markdown file
- Add executable code blocks:
```python exec
x = 1 + 1
```- (Optional) Capture output:
<!-- id:result -->old<!-- /id:result -->
```python exec output-id=result
print(2 + 2)
```- Run:
mdexec notebook.md
- Markdown code blocks can be marked as executable using the
execattribute. exectellsmdexecto run the block. Skip a code block by removingexecor setting it toexec=false.
```python exec
print("Hello from Python")
```
```bash exec
echo Hello from Bash
```The id is the glue between blocks.
- A code block that gets executed (has
exec) produces output.stdoutcan be captured and put back into the markdown document. - Add
output-id=other-idto an executable code block to redirect its output somewhere else, another code block, or an html tagged region. - A code block is identified with the attribute
id=myid. - An HTML block is identified with special tags:
< !-- id:myid -->and< !-- /id:myid -->. - Any block that has an ID can be used as input to, or output from an executed code block.
For example, this code outputs to the html tagged region below it.
```python exec output-id=report
print("Generated report")
```
```html
<!-- id:report -->
Generated report
<!-- /id:report -->
```- One code block maps to one output target.
- Think of it as "write to this location".
Output from a code block can optionally be written back to the markdown file with either HTML comment blocks or code blocks.
In both cases a code block must execute and produce some output. For example,
```bash exec output-id=example
echo Hello
```Outputting between HTML comments allows for writing arbitrary markdown content.
Before execution:
<!-- id:example -->
<!-- /id:example -->After execution:
<!-- id:example -->
Hello
<!-- /id:example -->- The content between the comments is replaced.
- The
output-idvalue in the source code block must match the ID in the comment.
Similarly, output can be written into another code block with matching IDs:
Before Execution:
```id=example
```After execution:
```id=example
Hello
```- For structured output like JSON, the ID should come somewhere after the language. E.g.
json id=example.
mdexec tries to preserve formatting:
- If your output has newlines, they are kept
- If your block is multiline, output will match that style
- No extra whitespace is added unless needed for valid Markdown
This means:
print("hello")
print()
print()will produce:
hello
Blocks are executed in the order they appear in the file.
- Later code blocks can depend on earlier ones (using a shared context, e.g.
ctx=somethingin both code blocks) - Output (code and HTML) blocks can appear in any order
- Keep this in mind when structuring notebooks
- By default, Python code blocks don't carry function definitions or variables to other code blocks.
- A shared context can be declared with the attribute
ctx=some-name.
```python exec ctx=calc
x = 10
```
```python exec ctx=calc
y = x + 5
print(y)
```- A global
mdAPI object is injected into Python code blocks to programmatically read and write to the Markdown document. - Use
md.get('some-id').contentto reference old content within Python code. - Use
md.set('some-id, 'new-content')to rederence content within Python code.
Given some input data:
```json id=data
{
"value": 123,
"hello": true
}
```
Perform some calculation with it:
```python exec
import json
data = json.loads(md.get('data').content)
x = data['value'] * 456
```Or even update cells of tables:
| Item | Price |
|------|-------|
| A | <!-- id:a -->0<!-- /id:a --> |
| B | <!-- id:b -->0<!-- /id:b --> |
```python exec
prices = {"a": 10, "b": 20}
for k, v in prices.items():
md.set(k, v)
```mdexec is designed to be simple and composable. For auto-reloading, you can pair it with tools like entr:
ls notebook.md | entr mdexec notebook.md- Execute Python and Bash code blocks
- Write output anywhere in the document
- Shared or isolated execution context across blocks
- Minimal, readable Markdown (no hidden formats)
- Line-preserving updates (no full rewrites)
- Zero dependencies
- Markdown is the source of truth
- Execution should be transparent and inspectable
- Outputs should be stable and version-control friendly
- No hidden state, no magic files