Use cogapp to auto-generate sections of markdown documentation by embedding Python code that produces content...
Use cogapp to auto-generate sections of markdown files. Cog lets you embed Python code in markdown that produces output inline — the generated content lives in the file alongside the code that created it. Running cog -r regenerates the output, keeping docs in sync with code.
pip install cogapp
Or add "cogapp" to your [dependency-groups] dev dependencies in pyproject.toml.
Use HTML comments as cog markers so the Python code is invisible when the markdown is rendered:
<!-- [[[cog
import cog
cog.outl("This content is generated by cog.")
]]] -->
This content is generated by cog.
<!-- [[[end]]] -->
The pattern is:
<!-- [[[cog — opens the Python code block (hidden in rendered markdown)cog.out() or cog.outl() to produce output]]] --> — closes the Python code block<!-- [[[end]]] --> — marks the end of the generated regionRegenerate all cog blocks in-place:
cog -r docs/*.md
Or without installing — use uv run --with to run cog in a temporary environment:
uv run --with cogapp cog -r docs/*.md
The -r flag replaces file contents in-place. Without it, cog writes to stdout.
The most common use is keeping CLI documentation in sync with actual --help output.
For Click-based CLIs, use CliRunner to capture help output directly (no subprocess needed):
<!-- [[[cog
import cog
from mypackage import cli
from click.testing import CliRunner
runner = CliRunner()
result = runner.invoke(cli.cli, ["mycommand", "--help"])
help = result.output.replace("Usage: cli", "Usage: mypackage")
cog.out(
"```\n{}\n```\n".format(help.strip())
)
]]] -->
<!-- [[[end]]] -->
The replace("Usage: cli", "Usage: mypackage") is needed because CliRunner reports the command name as cli instead of the real entry point name.
For argparse or other CLIs, use subprocess:
<!-- [[[cog
import cog
import subprocess
result = subprocess.run(["mycommand", "--help"], capture_output=True, text=True)
cog.out(
"```\n{}\n```\n".format(result.stdout.strip())
)
]]] -->
<!-- [[[end]]] -->
Add a step to your test workflow that checks all cog blocks are up to date. This fails CI if someone changes CLI behavior but forgets to regenerate the docs:
- name: Check if cog needs to be run
run: |
cog --check docs/*.md
cog --check exits with code 1 if any file would change. It does not modify files.
Use --check-fail-msg to tell developers how to fix it:
- name: Check if cog needs to be run
run: |
cog --check --check-fail-msg='Run "cog -r docs/*.md" to update' docs/*.md
If the project uses uv and cogapp is not a declared dependency, use uv run --with:
- name: Check if cog needs to be run
run: |
uv run --with cogapp cog --check docs/*.md
<!-- [[[cog
import cog
headers = ["Name", "Type", "Description"]
rows = [
["id", "int", "Primary key"],
["name", "str", "User name"],
]
cog.outl("| " + " | ".join(headers) + " |")
cog.outl("| " + " | ".join(["---"] * len(headers)) + " |")
for row in rows:
cog.outl("| " + " | ".join(row) + " |")
]]] -->
<!-- [[[end]]] -->
<!-- [[[cog
import cog
import json
data = {"key": "value", "count": 42}
cog.out("```json\n")
cog.outl(json.dumps(data, indent=2))
cog.out("```\n")
]]] -->
<!-- [[[end]]] -->
Each cog block is independent. You can have many blocks in one file — each gets its own <!-- [[[cog ... ]]] --> and <!-- [[[end]]] --> pair. Imports are shared across blocks within the same file.
cog -r FILE — regenerate in-placecog --check FILE — check without modifying (for CI)cog --check --diff FILE — show what would changecog -P FILE — use print() instead of cog.outl() in code blocks