A sua documentação, testes e código são três ficheiros a contar a mesma história mal.
Atualiza a assinatura da função no source. Esqueces o exemplo do README. Uma semana depois, um novo colaborador copia o snippet obsoleto para produção. O teu ficheiro de teste ainda codifica o comportamento antigo como resultado esperado. Agora tens dois bugs e um ticket de documentação.
Este é o problema de sincronização. Toda a codebase o tem. A maioria das equipas resolve-o com disciplina, o que falha no momento em que alguém tem pressa.
A programação literária corrige-o ao tornar o ficheiro de prosa a fonte de verdade. Escreves um documento Markdown que os humanos leem. Uma pequena ferramenta extrai os blocos de código para ficheiros Python executáveis. Testes, implementação e explicação vivem todos no mesmo sítio. Alteras a prosa, e alteras o código.
O que é programação literária?
Donald Knuth cunhou o termo em 1984. A ideia era simples: escreve um programa como escreves um ensaio. Explica a lógica em linguagem natural, intercala o código, e deixa uma ferramenta chamada tangle extrair o source executável enquanto weave produz a documentação formatada.
As ferramentas clássicas, como o sistema WEB de Knuth, estavam fortemente acopladas a Pascal e TeX. Nunca se tornaram populares no desenvolvimento de software mainstream. O workflow parecia estranho, o tooling era pesado, e a maioria dos programadores preferia ler código num IDE, não num PDF.
Mas o insight central ainda é válido. A prosa e o código devem partilhar uma única fonte de verdade. As linguagens modernas e o Markdown tornam isto mais fácil do que Knuth poderia ter imaginado. Não precisas de um compiler especial. Precisas de cerca de trinta linhas de Python e um parser de Markdown.
Como um único ficheiro Markdown se torna uma test suite
O mecanismo é simples. Escreves um ficheiro .md com fenced code blocks. Um comentário de cabeçalho dentro de cada bloco diz ao extrator a que ficheiro pertence. O extrator concatena blocos com o mesmo cabeçalho num único module Python. Depois corres o pytest sobre o resultado.
Eis um exemplo completo. Guarda-o 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
Os comentários `# calc.py` e `# test_calc.py` não são sintaxe mágica. São convenções que o nosso script entende.
## O script de extração
Esta é a ferramenta completa. Guarda-a 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)
Corre-o:
$ 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
O diretório build/ agora contém calc.py e test_calc.py. Podes importá-los, fazer type-checking, ou distribuí-los como um package. O ficheiro Markdown é a fonte canónica. Tudo o resto é gerado.
Quando um ficheiro ajuda, e quando prejudica
Esta abordagem brilha para libraries, algoritmos, e qualquer coisa onde o porquê importa tanto como o quê. Documentação de API, código de investigação, e pipelines de configuração beneficiam todos de prosa que permanece ligada à implementação.
Não brilha para código de aplicação com muito boilerplate. Uma vista Django com quinze imports de decorators não precisa de um ensaio. Se o teu module é maioritariamente plumbing de framework, a sobrecarga da estrutura literária adiciona fricção sem clareza.
A outra limitação é o suporte de ferramentas. Os IDEs esperam encontrar o teu código em ficheiros .py. Jump-to-definition, inline linting, e autocomplete precisam todos de ficheiros gerados para existirem antes de funcionarem. Podes resolver isto correndo tangle.py como um pre-commit hook ou como parte do teu build step. Mas é um passo extra. Se a tua equipa já luta com complexidade de build, adicionar uma custom extraction pipeline pode não valer a pena.
Ferramentas reais que fazem isto
O script de trinta linhas acima é suficiente para começar. Se quiseres algo production-ready, há opções maduras.
Entangled é uma ferramenta moderna de programação literária que funciona com qualquer linguagem. Usa uma sintaxe ligeiramente diferente, mas a ideia é idêntica. Acompanha dependências entre blocos de código e suporta múltiplos ficheiros de saída.
Jupyter notebooks resolvem um problema semelhante para data science. Misturam prosa, código, e output num único ficheiro. A desvantagem é que os notebooks são terríveis para version control. Os diffs são ilegíveis, e os merge conflicts são comuns.
Org-mode com Babel é a implementação mais poderosa. Se já vives no Emacs, é imbatível. Se não, a curva de aprendizagem é íngreme.
Para a maioria das equipas, um simples extrator de Markdown é o meio-termo pragmático. Usa ferramentas que todos já entendem.
FAQ
Funciona isto com type checkers?
Sim. Gera os ficheiros .py primeiro, depois corre mypy ou pyright contra o diretório de build.
E quanto a linguagens que não sejam Python?
O script tangle.py é language-agnostic. Altera o regex de python para rust ou go e funciona da mesma maneira. Podes até misturar linguagens num documento.
Como lidar com imports entre blocos?
O extrator concatena todos os blocos com o mesmo nome de ficheiro pela ordem em que aparecem no Markdown. Mantém os teus imports no primeiro bloco para esse ficheiro, ou repete-os. Python lida com imports duplicados graciosamente.
Começa com um module
Não precisas de reescrever toda a codebase. Escolhe um module pequeno onde a documentação parece sempre ficar atrás do código. Converte-o num ficheiro Markdown, adiciona o script de extração à tua CI pipeline, e corre os teus testes contra o output gerado.
Se a documentação começar a manter-se atualizada sem lembretes, saberás que a abordagem funciona. Se parecer overhead, abandona-a. A programação literária é uma ferramenta, não uma religião. O objetivo não é impressionar Knuth. O objetivo é deixar de mentir a ti mesmo em três ficheiros diferentes.