Tus docs, tests y código son tres archivos contando la misma historia mal.
Actualizas la firma de la función en el source. Olvidas el ejemplo del README. Una semana después, un nuevo empleado copia el snippet obsoleto en producción. Tu archivo de test aún codifica el comportamiento antiguo como resultado esperado. Ahora tienes dos bugs y un ticket de documentación.
Este es el problema de sincronización. Toda codebase lo tiene. La mayoría de los equipos lo resuelven con disciplina, lo cual falla en el momento en que alguien tiene prisa.
La programación literaria lo arregla haciendo que el archivo de prosa sea la fuente de verdad. Escribes un documento Markdown que los humanos leen. Una pequeña herramienta extrae los bloques de código en archivos Python ejecutables. Tests, implementación y explicación viven en un solo lugar. Cambias la prosa, y cambias el código.
¿Qué es la programación literaria?
Donald Knuth acuñó el término en 1984. La idea era simple: escribe un programa como escribes un ensayo. Explica la lógica en lenguaje natural, intercala el código, y deja que una herramienta llamada tangle extraiga el source ejecutable mientras weave produce la documentación formateada.
Las herramientas clásicas, como el sistema WEB de Knuth, estaban fuertemente acopladas a Pascal y TeX. Nunca se popularizaron en el desarrollo de software mainstream. El flujo de trabajo se sentía extraño, el tooling era pesado, y la mayoría de los programadores preferían leer código en un IDE, no en un PDF.
Pero la idea central sigue siendo válida. La prosa y el código deberían compartir una única fuente de verdad. Los lenguajes modernos y Markdown hacen esto más fácil de lo que Knuth podría haber imaginado. No necesitas un compiler especial. Necesitas unas treinta líneas de Python y un parser de Markdown.
Cómo un solo archivo Markdown se convierte en un suite de tests
El mecanismo es sencillo. Escribes un archivo .md con fenced code blocks. Un comentario de encabezado dentro de cada bloque le dice al extractor a qué archivo pertenece. El extractor concatena bloques con el mismo encabezado en un único module de Python. Luego ejecutas pytest sobre el resultado.
Aquí hay un ejemplo completo. Guárdalo como 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
Los comentarios `# calc.py` y `# test_calc.py` no son sintaxis mágica. Son convenciones que nuestro script entiende.
## El script de checkout
Esta es toda la herramienta. Guárdala como `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)
Ejecútalo:
$ 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
El directorio build/ ahora contiene calc.py y test_calc.py. Puedes importarlos, hacerles type-check o distribuirlos como un package. El archivo Markdown es la fuente canónica. Todo lo demás se genera.
Cuándo un archivo ayuda, y cuándo perjudica
Este enfoque brilla para libraries, algoritmos y cualquier cosa donde el por qué importa tanto como el qué. La documentación de API, el código de investigación y los pipelines de configuración se benefician de la prosa que permanece ligada a la implementación.
No brilla para código de aplicación con mucho boilerplate. Una view de Django con quince imports de decorators no necesita un ensayo. Si tu module es mayormente plumbing de framework, el overhead de la estructura literaria añade fricción sin claridad.
La otra limitación es el soporte de herramientas. Los IDEs esperan encontrar tu código en archivos .py. Jump-to-definition, inline linting y autocomplete necesitan que los archivos generados existan antes de funcionar. Puedes resolver esto ejecutando tangle.py como un pre-commit hook o como parte de tu build step. Pero es un paso extra. Si tu equipo ya lucha con la complejidad del build, agregar una custom extraction pipeline puede no valer la pena.
Herramientas reales que hacen esto
El script de treinta líneas de arriba es suficiente para empezar. Si quieres algo production-ready, hay opciones maduras.
Entangled es una herramienta moderna de programación literaria que funciona con cualquier lenguaje. Usa una sintaxis ligeramente diferente, pero la idea es idéntica. Rastrea dependencias entre bloques de código y soporta múltiples archivos de salida.
Jupyter notebooks resuelven un problema similar para data science. Mezclan prosa, código y output en un archivo. La desventaja es que los notebooks son terribles para version control. Los diffs son ilegibles y los merge conflicts son comunes.
Org-mode con Babel es la implementación más poderosa. Si ya vives en Emacs, es imbatible. Si no, la curva de aprendizaje es empinada.
Para la mayoría de los equipos, un simple extractor de Markdown es el punto medio pragmático. Usa herramientas que todos ya entienden.
FAQ
¿Funciona esto con type checkers?
Sí. Genera los archivos .py primero, luego ejecuta mypy o pyright contra el directorio de build.
¿Qué pasa con lenguajes que no sean Python?
El script tangle.py es language-agnostic. Cambia el regex de python a rust o go y funciona igual. Incluso puedes mezclar lenguajes en un documento.
¿Cómo manejo imports entre bloques?
El extractor concatena todos los bloques con el mismo nombre de archivo en el orden en que aparecen en el Markdown. Mantén tus imports en el primer bloque de ese archivo, o repítelos. Python maneja los imports duplicados gracefulmente.
Empieza con un module
No necesitas reescribir toda tu codebase. Elige un module pequeño donde la documentación siempre parece quedarse atrás del código. Conviértelo en un archivo Markdown, añade el script de checkout a tu CI pipeline, y ejecuta tus tests contra el output generado.
Si la documentación empieza a mantenerse actualizada sin recordatorios, sabrás que el enfoque funciona. Si se siente como overhead, abandónalo. La programación literaria es una herramienta, no una religión. El objetivo no es impresionar a Knuth. El objetivo es dejar de mentirte a ti mismo en tres archivos diferentes.