Your docs, tests, and code are three files telling the same story badly.

You update the function signature in the source. You forget the README example. A week later, a new hire copies the stale snippet into production. Your test file still encodes the old behavior as the expected result. Now you have two bugs and a documentation ticket.

This is the synchronization problem. Every codebase has it. Most teams solve it with discipline, which fails the moment someone is in a hurry.

Literate programming fixes it by making the prose file the source of truth. You write a Markdown document that humans read. A small tool extracts the code blocks into runnable Python files. Tests, implementation, and explanation all live in one place. Change the prose, and you change the code.

What is literate programming?

Donald Knuth coined the term in 1984. The idea was simple: write a program the way you write an essay. Explain the logic in natural language, interleave the code, and let a tool called tangle extract the executable source while weave produces the formatted documentation.

The classic tools, like Knuth’s WEB system, were tightly coupled to Pascal and TeX. They never caught on in mainstream software development. The workflow felt foreign, the tooling was heavy, and most programmers preferred to read code in an IDE, not a PDF.

But the core insight is still valid. Prose and code should share a single source of truth. Modern languages and Markdown make this easier than Knuth could have imagined. You do not need a special compiler. You need about thirty lines of Python and a Markdown parser.

How a single Markdown file becomes a test suite

The mechanism is straightforward. You write a .md file with fenced code blocks. A header comment inside each block tells the extractor which file it belongs to. The extractor concatenates blocks with the same header into a single Python module. Then you run pytest on the result.

Here is a complete example. Save this as calculator.md:

# Calculator: addition and its properties

The `add` function is trivial. That is the point. Even trivial code deserves context about why it exists and what invariants it preserves.

```python
# calc.py
def add(a: int, b: int) -> int:
    """Return the sum of two integers."""
    return a + b

Addition commutes. We verify this explicitly because it is a property future maintainers might break by accident if they switch to a different implementation.

# test_calc.py
from calc import add

def test_addition_commutes():
    assert add(2, 3) == add(3, 2)

def test_identity():
    assert add(7, 0) == 7

The `# calc.py` and `# test_calc.py` comments are not magic syntax. They are conventions that our script understands.

## The extraction script

This is the entire tool. Save it as `tangle.py`:

```python
import re
import tempfile
import subprocess
from pathlib import Path


def tangle(md_path: Path, out_dir: Path) -> Path:
    """Extract code blocks from Markdown into runnable Python files."""
    content = md_path.read_text()
    files: dict[str, list[str]] = {}

    # Extract all python code blocks
    for block in re.findall(r"```python\n(.*?)```", content, re.DOTALL):
        lines = block.strip().split("\n")

        # First line like '# filename.py' sets the target file
        if lines and lines[0].startswith("# "):
            filename = lines[0][2:].strip()
            code = lines[1:]
        else:
            filename = "module.py"
            code = lines

        files.setdefault(filename, []).extend(code)

    # Write extracted files
    out_dir.mkdir(parents=True, exist_ok=True)
    for filename, code_lines in files.items():
        (out_dir / filename).write_text("\n".join(code_lines) + "\n")

    return out_dir


if __name__ == "__main__":
    out = tangle(Path("calculator.md"), Path("build"))
    result = subprocess.run(
        ["python", "-m", "pytest", str(out), "-v"],
        capture_output=False,
    )
    raise SystemExit(result.returncode)

Run it:

$ python tangle.py
============================= test session starts ==============================
build/test_calc.py::test_addition_commutes PASSED
build/test_calc.py::test_identity PASSED
============================== 2 passed in 0.01s

The build/ directory now contains calc.py and test_calc.py. You can import them, type-check them, or ship them as a package. The Markdown file is the canonical source. Everything else is generated.

When one file helps, and when it hurts

This approach shines for libraries, algorithms, and anything where the why matters as much as the what. API documentation, research code, and configuration pipelines all benefit from prose that stays locked to the implementation.

It does not shine for boilerplate-heavy application code. A Django view with fifteen decorator imports does not need an essay. If your module is mostly framework plumbing, the overhead of literate structure adds friction without clarity.

The other limitation is tool support. IDEs expect to find your code in .py files. Jump-to-definition, inline linting, and autocomplete all need generated files to exist before they work. You can solve this by running tangle.py as a pre-commit hook or as part of your build step. But it is an extra step. If your team already struggles with build complexity, adding a custom extraction pipeline might not be worth it.

Real tools that do this

The thirty-line script above is enough to get started. If you want something production-ready, there are mature options.

Entangled is a modern literate programming tool that works with any language. It uses a slightly different syntax, but the idea is identical. It tracks dependencies between code blocks and supports multiple output files.

Jupyter notebooks solve a similar problem for data science. They mix prose, code, and output in one file. The downside is that notebooks are terrible for version control. Diffs are unreadable, and merge conflicts are common.

Org-mode with Babel is the most powerful implementation. If you already live in Emacs, it is unbeatable. If you do not, the learning curve is steep.

For most teams, a simple Markdown extractor is the pragmatic middle ground. It uses tools everyone already understands.

FAQ

Does this work with type checkers?

Yes. Generate the .py files first, then run mypy or pyright against the build directory.

What about non-Python languages?

The tangle.py script is language-agnostic. Change the regex from python to rust or go and it works the same way. You can even mix languages in one document.

How do I handle imports across blocks?

The extractor concatenates all blocks with the same filename in the order they appear in the Markdown. Keep your imports in the first block for that file, or repeat them. Python handles duplicate imports gracefully.

Start with one module

You do not need to rewrite your whole codebase. Pick one small module where the documentation always seems to lag behind the code. Convert it to a Markdown file, add the extraction script to your CI pipeline, and run your tests against the generated output.

If the docs start staying current without reminders, you will know the approach is working. If it feels like overhead, abandon it. Literate programming is a tool, not a religion. The goal is not to impress Knuth. The goal is to stop lying to yourself in three different files.