O pyproject.toml é o arquivo central para declarar como um projeto Python é construído, quais são seus metadados e como ferramentas compatíveis devem funcionar. A tabela [build-system] escolhe o backend de build; [project] descreve nome, versão, compatibilidade e dependências; [tool.*] guarda configurações específicas. Em aplicações, o foco costuma ser execução reproduzível. Em bibliotecas, compatibilidade e metadados de distribuição pesam mais.

Este guia explica a configuração do projeto, não o processo de enviar artefatos a um índice. Para essa etapa separada, consulte como publicar um pacote no PyPI. Antes, vale dominar módulos e pacotes, ambientes virtuais e gerenciamento de dependências com pip.

O que o arquivo resolve

Historicamente, projetos distribuíam informações entre setup.py, setup.cfg, requirements.txt e arquivos próprios de ferramentas. O pyproject.toml oferece pontos padronizados, mas não transforma tudo em um único padrão. [project] segue a especificação de metadados; [tool.ruff], por exemplo, pertence ao Ruff e não ao Python.

TOML usa pares, listas e tabelas. Comentários começam com #, strings têm aspas e nomes pontuados representam níveis. Valide o arquivo com as próprias ferramentas, pois TOML sintaticamente válido ainda pode conter uma chave desconhecida.

A guia oficial da PyPA para escrever pyproject.toml apresenta escolhas atuais, enquanto a especificação de pyproject.toml define o contrato normativo. A motivação para builds isolados está na PEP 518.

build-system: como o projeto é construído

Um exemplo com setuptools:

[build-system]
requires = ["setuptools>=77"]
build-backend = "setuptools.build_meta"

requires contém ferramentas necessárias para executar o backend em um ambiente isolado. Não coloque ali Flask, requests ou dependências usadas pela aplicação: elas pertencem a [project].dependencies. O frontend de build cria um ambiente, instala os requisitos e chama build-backend para gerar wheel ou sdist.

Outros backends usam valores diferentes. Copie a configuração da documentação do backend escolhido em vez de combinar chaves de ferramentas incompatíveis. Limites mínimos devem corresponder aos recursos utilizados; fixar exatamente toda dependência de build pode dificultar manutenção, enquanto deixar qualquer versão pode quebrar builds antigos.

Uma aplicação que roda apenas como conjunto de scripts talvez não precise ser construída. Porém, transformá-la em pacote instalável reduz importações dependentes do diretório atual e facilita testes. O layout src ajuda a garantir que o código testado seja o instalado.

project: identidade e requisitos

Uma biblioteca pequena pode declarar:

[project]
name = "texto-claro"
version = "0.3.0"
description = "Utilitários pequenos para normalizar texto"
readme = "README.md"
requires-python = ">=3.10"
license = "MIT"
authors = [{ name = "Equipe Texto Claro" }]
dependencies = [
  "regex>=2024.5",
]

[project.optional-dependencies]
cli = ["rich>=13"]

[project.scripts]
texto-claro = "texto_claro.cli:main"

name identifica a distribuição, não necessariamente o módulo importado. requires-python impede instalação em interpretadores incompatíveis. dependencies deve conter somente o necessário em tempo de execução. Extras oferecem capacidades opcionais instaláveis, por exemplo pip install 'texto-claro[cli]'. Um script mapeia o comando para uma função importável.

Metadados podem ser estáticos ou listados em dynamic. Prefira estáticos quando possível: são mais fáceis de inspecionar sem executar backend. Se a versão vier do código ou de uma ferramenta, declare dynamic = ["version"] e configure o mecanismo segundo o backend. Nunca forneça ao mesmo tempo um valor estático e o marque como dinâmico.

Aplicações e bibliotecas pedem decisões diferentes

Uma biblioteca deve aceitar uma faixa razoável de versões de dependências e testar os limites relevantes. Fixar requests==... em dependencies transfere um conflito desnecessário aos consumidores. Defina mínimo quando o código depende de um recurso e máximo apenas quando há incompatibilidade conhecida.

Uma aplicação controla o ambiente final e busca uma resolução reproduzível. O pyproject.toml pode declarar faixas diretas, enquanto um arquivo de lock registra versões exatas, hashes e dependências transitivas. O lock não é substituído por [project].dependencies. O gerenciador escolhido determina formato e atualização do lock.

Dependências de desenvolvimento, como pytest e Ruff, não são requisitos de execução. Com ferramentas que suportam o padrão de grupos:

[dependency-groups]
test = ["pytest>=8", "pytest-cov>=5"]
lint = ["ruff>=0.12"]
dev = [
  { include-group = "test" },
  { include-group = "lint" },
]

Grupos servem ao desenvolvimento local e CI e não viram extras publicados. Verifique o suporte do instalador usado. Se ele não reconhecer grupos, adote seu mecanismo documentado, sem inventar uma tabela parecida.

Configurações em tool

Cada ferramenta possui seu namespace. Uma base enxuta para pytest e Ruff:

[tool.pytest.ini_options]
addopts = "-ra --strict-markers"
testpaths = ["tests"]

[tool.ruff]
line-length = 100
target-version = "py310"

[tool.ruff.lint]
select = ["E", "F", "I", "B"]

Não presuma que nomes são compartilhados: target-version e exclusões podem ter sintaxe diferente entre Ruff, mypy e ferramentas de cobertura. Consulte a documentação e mantenha perto apenas configurações que o projeto realmente precisa. Duplicar defaults aumenta ruído e impede perceber decisões importantes.

O pytest também pode ser aprofundado no guia de testes automatizados com pytest. Em CI, execute a instalação e os comandos reais, como descrito no artigo de GitHub Actions para Python.

Como validar o projeto em um ambiente limpo

Ler o TOML não basta para confirmar que ele funciona. O teste mais útil reproduz o caminho de um usuário: gerar os artefatos, instalar o wheel e importar o pacote fora da árvore do repositório. Instale a ferramenta de build no ambiente de desenvolvimento e execute:

python -m pip install build
python -m build
python -m pip install --force-reinstall dist/*.whl
python -c "import texto_claro; print(texto_claro.__name__)"

O comando de build deve produzir um wheel e, em geral, uma distribuição de código-fonte. Examine o conteúdo antes de publicar: arquivos de pacote, licença, README e metadados necessários devem estar presentes; testes, caches, segredos e artefatos locais não. Um wheel que compila no diretório do projeto, mas falha depois de instalado, costuma indicar configuração incorreta de descoberta de pacotes ou dependência acidental do diretório atual.

Para bibliotecas, teste também a instalação a partir da distribuição de código-fonte. Esse caminho obriga o frontend a recriar o wheel e revela requisitos de build ausentes. Em aplicações, crie um ambiente vazio usando a versão mínima de Python declarada, instale o projeto pelo procedimento documentado e execute ao menos o comando de inicialização e os testes essenciais.

Há três camadas de validação diferentes. Um parser TOML detecta sintaxe inválida. O backend verifica se entende as tabelas de build e metadados. A instalação e a execução confirmam que o artefato representa de fato o projeto. Manter essas verificações na CI é mais confiável do que depender de uma instalação editável, pois o modo editável pode tornar arquivos disponíveis mesmo quando eles não entrariam no wheel.

Armadilhas frequentes

  • Colocar dependências da aplicação em [build-system].requires.
  • Usar versões exatas de bibliotecas em project.dependencies sem necessidade.
  • Confundir extra opcional com grupo de desenvolvimento.
  • Declarar versão estática e dinâmica simultaneamente.
  • Copiar tabelas [tool.*] sem instalar ou entender a ferramenta.
  • Acreditar que pyproject.toml é um lock reproduzível.
  • Escolher requires-python diferente das versões realmente testadas.
  • Misturar configuração de backends distintos.

Checklist de revisão

  • O backend e seus requisitos estão corretos e suficientes.
  • Metadados são estáticos sempre que viável.
  • Dependências de execução não incluem ferramentas de desenvolvimento.
  • Extras representam recursos opcionais para usuários.
  • Grupos representam tarefas internas e têm suporte do instalador.
  • Aplicações mantêm uma estratégia explícita de lock.
  • Bibliotecas testam a faixa de Python e dependências declarada.
  • Cada tabela tool corresponde à documentação atual.
  • Build, instalação e testes rodam em ambiente limpo.

Comece com o mínimo exigido pelo projeto. Adicione configurações quando uma necessidade concreta aparecer, valide em um ambiente virtual vazio e mantenha build, execução e desenvolvimento como responsabilidades distintas. Essa separação torna o arquivo legível e evita que decisões de uma aplicação sejam aplicadas por engano a uma biblioteca.