Ihre Docs, Tests und Code sind drei Dateien, die dieselbe Geschichte schlecht erzählen.

Sie aktualisieren die Funktionssignatur im Source. Sie vergessen das README-Beispiel. Eine Woche später kopiert ein neuer Mitarbeiter das veraltete Snippet in die Produktion. Ihre Testdatei codiert das alte Verhalten weiterhin als erwartetes Ergebnis. Jetzt haben Sie zwei Bugs und ein Dokumentationsticket.

Das ist das Synchronisierungsproblem. Jede Codebase hat es. Die meisten Teams lösen es mit Disziplin, was scheitert, sobald jemand in Eile ist.

Literate Programming behebt es, indem die Prosadatei zur single source of truth wird. Sie schreiben ein Markdown-Dokument, das Menschen lesen. Ein kleines Tool extrahiert die Code-Blöcke in ausführbare Python-Dateien. Tests, Implementierung und Erklärung leben an einem Ort. Ändern Sie die Prosa, und Sie ändern den Code.

Was ist Literate Programming?

Donald Knuth prägte den Begriff 1984. Die Idee war einfach: Schreiben Sie ein Programm so, wie Sie einen Aufsatz schreiben. Erklären Sie die Logik in natürlicher Sprache, verschachteln Sie den Code, und lassen Sie ein Tool namens tangle den ausführbaren Source extrahieren, während weave die formatierte Dokumentation erzeugt.

Die klassischen Tools, wie Knuths WEB-System, waren eng an Pascal und TeX gekoppelt. Sie haben sich in der Mainstream-Softwareentwicklung nie durchgesetzt. Der Workflow fühlte sich fremd an, das Tooling war schwerfällig, und die meisten Programmierer bevorzugten es, Code in einer IDE zu lesen, nicht in einem PDF.

Aber die Kern-Erkenntnis ist immer noch gültig. Prosa und Code sollten eine einzige Quelle der Wahrheit teilen. Moderne Sprachen und Markdown machen das einfacher, als Knuth es sich hätte vorstellen können. Sie brauchen keinen speziellen Compiler. Sie brauchen etwa dreißig Zeilen Python und einen Markdown-Parser.

Wie eine einzelne Markdown-Datei zu einer Test-Suite wird

Der Mechanismus ist unkompliziert. Sie schreiben eine .md-Datei mit fenced code blocks. Ein Header-Kommentar innerhalb jedes Blocks sagt dem Extraktor, zu welcher Datei er gehört. Der Extraktor verkettet Blöcke mit demselben Header zu einem einzigen Python-Modul. Dann führen Sie pytest auf dem Ergebnis aus.

Hier ist ein vollständiges Beispiel. Speichern Sie das als 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

Die `# calc.py`- und `# test_calc.py`-Kommentare sind keine magische Syntax. Sie sind Konventionen, die unser Skript versteht.

## Das Extraktionsskript

Das ist das gesamte Tool. Speichern Sie es als `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)

Führen Sie es aus:

$ 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

Das build/-Verzeichnis enthält nun calc.py und test_calc.py. Sie können sie importieren, type-checken oder als package ausliefern. Die Markdown-Datei ist die kanonische Quelle. Alles andere wird generiert.

Wann eine Datei hilft, und wann sie schadet

Dieser Ansatz brilliert für Libraries, Algorithmen und alles, wo das Warum genauso wichtig ist wie das Was. API-Dokumentation, Forschungscode und Konfigurations-Pipelines profitieren alle von Prosa, die an die Implementierung gekoppelt bleibt.

Er brilliert nicht für boilerplate-lastigen Applikationscode. Eine Django-View mit fünfzehn Decorator-Imports braucht keinen Aufsatz. Wenn Ihr Modul hauptsächlich Framework-Plumbing ist, fügt der Overhead der literate-Struktur Reibung ohne Klarheit hinzu.

Die andere Einschränkung ist die Tool-Unterstützung. IDEs erwarten, Ihren Code in .py-Dateien zu finden. Jump-to-definition, inline linting und Autocomplete funktionieren alle nur, wenn generierte Dateien vorher existieren. Sie können das lösen, indem Sie tangle.py als pre-commit hook oder als Teil Ihres Build-Schritts ausführen. Aber es ist ein zusätzlicher Schritt. Wenn Ihr Team bereits mit Build-Komplexität kämpft, lohnt sich das Hinzufügen einer custom extraction pipeline vielleicht nicht.

Echte Tools, die das machen

Das dreißigzeilige Skript oben reicht, um loszulegen. Wenn Sie etwas Production-ready wollen, gibt es reife Optionen.

Entangled ist ein modernes Literate-Programming-Tool, das mit jeder Sprache funktioniert. Es verwendet eine leicht andere Syntax, aber die Idee ist identisch. Es verfolgt dependencies zwischen Code-Blöcken und unterstützt mehrere Output-Dateien.

Jupyter Notebooks lösen ein ähnliches Problem für Data Science. Sie mischen Prosa, Code und Output in einer Datei. Der Nachteil ist, dass Notebooks für Version Control schrecklich sind. Diffs sind unlesbar, und Merge-Konflikte sind häufig.

Org-mode mit Babel ist die leistungsfähigste Implementierung. Wenn Sie bereits in Emacs leben, ist es unschlagbar. Wenn nicht, ist die Lernkurve steil.

Für die meisten Teams ist ein einfacher Markdown-Extractor der pragmatische Mittelweg. Er verwendet Tools, die jeder bereits versteht.

FAQ

Funktioniert das mit Type Checkern?

Ja. Generieren Sie zuerst die .py-Dateien, dann führen Sie mypy oder pyright gegen das Build-Verzeichnis aus.

Was ist mit anderen Sprachen als Python?

Das tangle.py-Skript ist sprachunabhängig. Ändern Sie den Regex von python zu rust oder go, und es funktioniert gleich. Sie können sogar Sprachen in einem Dokument mischen.

Wie handle ich Imports über Blöcke hinweg?

Der Extraktor verkettet alle Blöcke mit demselben Dateinamen in der Reihenfolge, in der sie im Markdown erscheinen. Halten Sie Ihre Imports im ersten Block für diese Datei, oder wiederholen Sie sie. Python handhabt doppelte Imports graceful.

Beginnen Sie mit einem Modul

Sie müssen nicht Ihre gesamte Codebase umschreiben. Wählen Sie ein kleines Modul, bei dem die Dokumentation immer hinter dem Code zurückzufallen scheint. Konvertieren Sie es in eine Markdown-Datei, fügen Sie das Extraktionsskript zu Ihrer CI-Pipeline hinzu, und führen Sie Ihre Tests gegen den generierten Output aus.

Wenn die Docs anfangen, ohne Erinnerungen aktuell zu bleiben, wissen Sie, dass der Ansatz funktioniert. Wenn es sich wie Overhead anfühlt, lassen Sie es sein. Literate Programming ist ein Tool, keine Religion. Das Ziel ist nicht, Knuth zu beeindrucken. Das Ziel ist, aufzuhören, sich selbst in drei verschiedenen Dateien anzulügen.