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.