O que é pyproject.toml e Por Que Importa
O arquivo pyproject.toml é o coração da configuração moderna de projetos Python. Introduzido pela PEP 518 em 2016 e expandido pela PEP 517 e PEP 621, ele substitui a fragmentação de configurações espalhadas por arquivos como setup.py, setup.cfg, requirements.txt e tox.ini. Trata-se de um arquivo de configuração em formato TOML (Tom's Obvious, Minimal Language) que centraliza metadados do projeto, dependências, ferramentas de build e configurações de linters, testadores e outras utilidades.
Por que isso importa? Imagine um projeto onde você precisa atualizar versões em três arquivos diferentes, ou onde um colega novo não sabe aonde procurar a configuração do seu formatter de código. O pyproject.toml resolve isso: uma única fonte de verdade. Além disso, esse padrão alinha Python com outras linguagens como Node.js (package.json) e Rust (Cargo.toml), tornando a experiência mais intuitiva para desenvolvedores que transitam entre ecossistemas.
Estrutura Básica e Metadados do Projeto
A Estrutura Fundamental
Um pyproject.toml funcional começa com a definição da ferramenta de build e os metadados essenciais do projeto. A estrutura segue a convenção de seções entre colchetes, onde cada seção define configurações para um contexto específico.
[build-system]
requires = ["setuptools>=61.0", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "meu-projeto-incrivel"
version = "0.1.0"
description = "Uma biblioteca para processar dados com elegância"
readme = "README.md"
requires-python = ">=3.9"
license = {text = "MIT"}
authors = [
{name = "João Silva", email = "joao@example.com"}
]
keywords = ["data", "processing", "python"]
classifiers = [
"Development Status :: 3 - Alpha",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
]
A seção [build-system] define qual ferramenta constrói seu pacote (neste caso, setuptools) e sua versão mínima. A seção [project] contém os metadados PEP 621: nome, versão, descrição, autor, requisitos mínimos de Python e classifiers que ajudam plataformas como PyPI a categorizar seu projeto. O campo requires-python é crítico: ele previne que usuários com Python 3.8 instalem sua biblioteca se você usa features do 3.9.
Adicionando Dependências
As dependências são organizadas em grupos: dependências principais (necessárias para qualquer um que instale seu pacote) e dependências opcionais (grupos temáticos para desenvolvimento, testes, documentação, etc).
[project]
dependencies = [
"requests>=2.28.0",
"pydantic>=2.0",
"numpy>=1.20,<2.0",
]
[project.optional-dependencies]
dev = [
"pytest>=7.0",
"pytest-cov>=4.0",
"black>=23.0",
"ruff>=0.1.0",
"mypy>=1.0",
]
docs = [
"sphinx>=5.0",
"sphinx-rtd-theme>=1.0",
]
test = [
"pytest>=7.0",
"pytest-cov>=4.0",
"faker>=18.0",
]
Essa abordagem é clara: alguém pode instalar seu projeto com pip install meu-projeto-incrivel (apenas dependências principais) ou com pip install meu-projeto-incrivel[dev] (para desenvolvimento) ou pip install meu-projeto-incrivel[dev,docs] (múltiplos grupos). Note que estamos sendo explícitos com versões: >=2.28.0 garante compatibilidade para frente (até certo ponto), enquanto <2.0 em numpy previne quebras de API.
Configuração de Ferramentas de Desenvolvimento
Integrando Black, Ruff e MyPy
Python moderno exige verificação de tipo, formatação consistente e linting automático. Em vez de arquivos espalhados como .flake8, .isort.cfg e mypy.ini, você centraliza tudo no pyproject.toml. Isso reduz fricção e evita conflitos de configuração.
[tool.black]
line-length = 100
target-version = ['py39', 'py310', 'py311']
include = '\.pyi?$'
extend-exclude = '''
/(
# Diretórios
\.git
| \.hg
| \.mypy_cache
| \.tox
| \.venv
| build
| dist
)/
'''
[tool.ruff]
line-length = 100
target-version = "py39"
select = [
"E", # pycodestyle errors
"W", # pycodestyle warnings
"F", # Pyflakes
"I", # isort
"C", # flake8-comprehensions
"B", # flake8-bugbear
"UP", # pyupgrade
]
ignore = ["E501"] # Black já cuida de linhas longas
[tool.mypy]
python_version = "3.9"
warn_return_any = true
warn_unused_configs = true
disallow_untyped_defs = true
disallow_incomplete_defs = true
check_untyped_defs = true
no_implicit_optional = true
warn_redundant_casts = true
warn_unused_ignores = true
warn_no_return = true
exclude = ["tests/", "build/", "dist/"]
Com essa configuração, você roda black . para formatar tudo, ruff check . para encontrar problemas de qualidade, e mypy . para verificar tipos. As ferramentas automaticamente leem suas preferências do pyproject.toml. A escolha de line-length = 100 é opinião pessoal minha após anos no mercado: 80 é muito restrictivo para código moderno, 120 é muito solto. 100 é o ponto doce.
Pytest e Cobertura de Testes
[tool.pytest.ini_options]
minversion = "7.0"
addopts = "-ra -q --strict-markers --disable-warnings"
testpaths = ["tests"]
python_files = ["test_*.py", "*_test.py"]
markers = [
"slow: marcação para testes lentos",
"integration: testes que requerem conexão externa",
]
[tool.coverage.run]
source = ["src"]
omit = ["*/tests/*", "*/migrations/*"]
[tool.coverage.report]
exclude_lines = [
"pragma: no cover",
"def __repr__",
"raise AssertionError",
"raise NotImplementedError",
"if __name__ == .__main__.:",
"if TYPE_CHECKING:",
"if typing.TYPE_CHECKING:",
]
precision = 2
Dessa forma, pytest e coverage sabem exatamente onde procurar seus testes e qual código medir. O campo markers documenta quais tags você usa (útil para rodar pytest -m "not slow" em CI para feedback rápido). A seção coverage.report exclui padrões que inflam artificialmente números de cobertura sem valor real (como __repr__ ou imports para type checking).
Publicação e Distribuição
Configurando o Projeto para PyPI
Quando você quer compartilhar seu código com o mundo, é preciso pensar em como será descoberto e instalado. A seção [project.urls] ajuda nisso, assim como configurações de entrada do seu pacote.
[project.urls]
Homepage = "https://github.com/usuario/meu-projeto-incrivel"
Documentation = "https://meu-projeto.readthedocs.io"
Repository = "https://github.com/usuario/meu-projeto-incrivel.git"
"Bug Tracker" = "https://github.com/usuario/meu-projeto-incrivel/issues"
Changelog = "https://github.com/usuario/meu-projeto-incrivel/releases"
[project.scripts]
meu-comando = "meu_projeto.cli:main"
[project.gui-scripts]
meu-app = "meu_projeto.gui:launch"
O campo [project.scripts] define comandos CLI que será acessíveis depois da instalação. Se alguém instalar seu pacote, poderá simplesmente digitar meu-comando no terminal — setuptools cuida de criar um wrapper executável apropriado. URLs são essenciais para que PyPI exiba links apropriados na página do seu projeto.
Exemplo Prático: Arquivo Completo
Aqui está um pyproject.toml realista e completo de um projeto médio:
[build-system]
requires = ["setuptools>=61.0", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "data-pipeline"
version = "1.2.3"
description = "ETL simplificado para pipelines de dados"
readme = "README.md"
requires-python = ">=3.9"
license = {text = "Apache-2.0"}
authors = [
{name = "Maria Dev", email = "maria@company.com"}
]
keywords = ["etl", "data", "pipeline"]
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"License :: OSI Approved :: Apache Software License",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
]
dependencies = [
"pandas>=1.5.0",
"sqlalchemy>=2.0",
"python-dotenv>=0.20.0",
]
[project.optional-dependencies]
dev = [
"pytest>=7.0",
"pytest-cov>=4.0",
"black>=23.0",
"ruff>=0.1.0",
"mypy>=1.0",
]
docs = [
"sphinx>=5.0",
"sphinx-rtd-theme>=1.3",
]
postgres = [
"psycopg2-binary>=2.9",
]
[project.urls]
Homepage = "https://github.com/company/data-pipeline"
Documentation = "https://data-pipeline.readthedocs.io"
Repository = "https://github.com/company/data-pipeline.git"
"Bug Tracker" = "https://github.com/company/data-pipeline/issues"
[project.scripts]
pipeline = "data_pipeline.cli:main"
[tool.setuptools]
packages = ["data_pipeline"]
[tool.black]
line-length = 100
target-version = ['py39', 'py310', 'py311']
[tool.ruff]
line-length = 100
target-version = "py39"
select = ["E", "W", "F", "I", "C", "B", "UP"]
[tool.mypy]
python_version = "3.9"
disallow_untyped_defs = true
check_untyped_defs = true
[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = "-ra -q"
[tool.coverage.run]
source = ["data_pipeline"]
[tool.coverage.report]
exclude_lines = [
"pragma: no cover",
"if TYPE_CHECKING:",
]
Esse arquivo é autossuficiente. Com apenas isso, alguém pode clonar seu repositório, rodar pip install -e .[dev], e ter um ambiente pronto para desenvolvimento. Não há setup.py boilerplate, não há requirements.txt desincronizado, não há setup.cfg misterioso.
Conclusão
Três pontos críticos aprendidos nesta jornada:
-
Centralização é poder: O
pyproject.tomlé a resposta moderna à fragmentação. Uma única fonte de verdade para metadados, dependências e configurações elimina conflitos e reduz erros. Você não precisa mais sincronizar versões entre múltiplos arquivos ou perguntar a colegas onde está a configuração do linter. -
Compatibilidade é intencional: Declarar explicitamente
requires-python, usar seções[project.optional-dependencies]e versionamento semântico não é burocracia — é comunicação clara com seus usuários sobre o que seu projeto oferece e exige. Isso constrói confiança e reduz surpresas ruins em produção. -
Ferramentas modernas convertem em prática: Usar Black, Ruff e MyPy em conjunto com uma configuração centralizada automatiza qualidade. Você ganha tempo para pensar em lógica, não em formatação ou bugs óbvios. Cada commit passa por um portão de qualidade consistente.
O pyproject.toml não é apenas um arquivo de configuração — é um contrato claro entre você e seus usuários/colaboradores.