Python

Guia Completo de pyproject.toml em Python: Configuração Centralizada de Projetos

14 min de leitura

Guia Completo de pyproject.toml em Python: Configuração Centralizada de Projetos

O que é pyproject.toml e Por Que Importa O arquivo é 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 , , e . 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 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 funcional começa com a definição da ferramenta

<h2>O que é pyproject.toml e Por Que Importa</h2>

<p>O arquivo <code>pyproject.toml</code> é 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 <code>setup.py</code>, <code>setup.cfg</code>, <code>requirements.txt</code> e <code>tox.ini</code>. Trata-se de um arquivo de configuração em formato TOML (Tom&#039;s Obvious, Minimal Language) que centraliza metadados do projeto, dependências, ferramentas de build e configurações de linters, testadores e outras utilidades.</p>

<p>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 <code>pyproject.toml</code> 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.</p>

<h2>Estrutura Básica e Metadados do Projeto</h2>

<h3>A Estrutura Fundamental</h3>

<p>Um <code>pyproject.toml</code> 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.</p>

<pre><code class="language-toml">[build-system]

requires = [&quot;setuptools&gt;=61.0&quot;, &quot;wheel&quot;]

build-backend = &quot;setuptools.build_meta&quot;

[project]

name = &quot;meu-projeto-incrivel&quot;

version = &quot;0.1.0&quot;

description = &quot;Uma biblioteca para processar dados com elegância&quot;

readme = &quot;README.md&quot;

requires-python = &quot;&gt;=3.9&quot;

license = {text = &quot;MIT&quot;}

authors = [

{name = &quot;João Silva&quot;, email = &quot;joao@example.com&quot;}

]

keywords = [&quot;data&quot;, &quot;processing&quot;, &quot;python&quot;]

classifiers = [

&quot;Development Status :: 3 - Alpha&quot;,

&quot;Intended Audience :: Developers&quot;,

&quot;License :: OSI Approved :: MIT License&quot;,

&quot;Programming Language :: Python :: 3&quot;,

&quot;Programming Language :: Python :: 3.9&quot;,

&quot;Programming Language :: Python :: 3.10&quot;,

&quot;Programming Language :: Python :: 3.11&quot;,

]</code></pre>

<p>A seção <code>[build-system]</code> define qual ferramenta constrói seu pacote (neste caso, setuptools) e sua versão mínima. A seção <code>[project]</code> 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 <code>requires-python</code> é crítico: ele previne que usuários com Python 3.8 instalem sua biblioteca se você usa features do 3.9.</p>

<h3>Adicionando Dependências</h3>

<p>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).</p>

<pre><code class="language-toml">[project]

dependencies = [

&quot;requests&gt;=2.28.0&quot;,

&quot;pydantic&gt;=2.0&quot;,

&quot;numpy&gt;=1.20,&lt;2.0&quot;,

]

[project.optional-dependencies]

dev = [

&quot;pytest&gt;=7.0&quot;,

&quot;pytest-cov&gt;=4.0&quot;,

&quot;black&gt;=23.0&quot;,

&quot;ruff&gt;=0.1.0&quot;,

&quot;mypy&gt;=1.0&quot;,

]

docs = [

&quot;sphinx&gt;=5.0&quot;,

&quot;sphinx-rtd-theme&gt;=1.0&quot;,

]

test = [

&quot;pytest&gt;=7.0&quot;,

&quot;pytest-cov&gt;=4.0&quot;,

&quot;faker&gt;=18.0&quot;,

]</code></pre>

<p>Essa abordagem é clara: alguém pode instalar seu projeto com <code>pip install meu-projeto-incrivel</code> (apenas dependências principais) ou com <code>pip install meu-projeto-incrivel[dev]</code> (para desenvolvimento) ou <code>pip install meu-projeto-incrivel[dev,docs]</code> (múltiplos grupos). Note que estamos sendo explícitos com versões: <code>&gt;=2.28.0</code> garante compatibilidade para frente (até certo ponto), enquanto <code>&lt;2.0</code> em numpy previne quebras de API.</p>

<h2>Configuração de Ferramentas de Desenvolvimento</h2>

<h3>Integrando Black, Ruff e MyPy</h3>

<p>Python moderno exige verificação de tipo, formatação consistente e linting automático. Em vez de arquivos espalhados como <code>.flake8</code>, <code>.isort.cfg</code> e <code>mypy.ini</code>, você centraliza tudo no <code>pyproject.toml</code>. Isso reduz fricção e evita conflitos de configuração.</p>

<pre><code class="language-toml">[tool.black]

line-length = 100

target-version = [&#039;py39&#039;, &#039;py310&#039;, &#039;py311&#039;]

include = &#039;\.pyi?$&#039;

extend-exclude = &#039;&#039;&#039;

/(

Diretórios

\.git

\.hg | \.mypy_cache | \.tox | \.venv | build | dist

)/

&#039;&#039;&#039;

[tool.ruff]

line-length = 100

target-version = &quot;py39&quot;

select = [

&quot;E&quot;, # pycodestyle errors

&quot;W&quot;, # pycodestyle warnings

&quot;F&quot;, # Pyflakes

&quot;I&quot;, # isort

&quot;C&quot;, # flake8-comprehensions

&quot;B&quot;, # flake8-bugbear

&quot;UP&quot;, # pyupgrade

]

ignore = [&quot;E501&quot;] # Black já cuida de linhas longas

[tool.mypy]

python_version = &quot;3.9&quot;

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 = [&quot;tests/&quot;, &quot;build/&quot;, &quot;dist/&quot;]</code></pre>

<p>Com essa configuração, você roda <code>black .</code> para formatar tudo, <code>ruff check .</code> para encontrar problemas de qualidade, e <code>mypy .</code> para verificar tipos. As ferramentas automaticamente leem suas preferências do <code>pyproject.toml</code>. A escolha de <code>line-length = 100</code> é opinião pessoal minha após anos no mercado: 80 é muito restrictivo para código moderno, 120 é muito solto. 100 é o ponto doce.</p>

<h3>Pytest e Cobertura de Testes</h3>

<pre><code class="language-toml">[tool.pytest.ini_options]

minversion = &quot;7.0&quot;

addopts = &quot;-ra -q --strict-markers --disable-warnings&quot;

testpaths = [&quot;tests&quot;]

python_files = [&quot;test_.py&quot;, &quot;_test.py&quot;]

markers = [

&quot;slow: marcação para testes lentos&quot;,

&quot;integration: testes que requerem conexão externa&quot;,

]

[tool.coverage.run]

source = [&quot;src&quot;]

omit = [&quot;/tests/&quot;, &quot;/migrations/&quot;]

[tool.coverage.report]

exclude_lines = [

&quot;pragma: no cover&quot;,

&quot;def __repr__&quot;,

&quot;raise AssertionError&quot;,

&quot;raise NotImplementedError&quot;,

&quot;if __name__ == .__main__.:&quot;,

&quot;if TYPE_CHECKING:&quot;,

&quot;if typing.TYPE_CHECKING:&quot;,

]

precision = 2</code></pre>

<p>Dessa forma, <code>pytest</code> e <code>coverage</code> sabem exatamente onde procurar seus testes e qual código medir. O campo <code>markers</code> documenta quais tags você usa (útil para rodar <code>pytest -m &quot;not slow&quot;</code> em CI para feedback rápido). A seção <code>coverage.report</code> exclui padrões que inflam artificialmente números de cobertura sem valor real (como <code>__repr__</code> ou imports para type checking).</p>

<h2>Publicação e Distribuição</h2>

<h3>Configurando o Projeto para PyPI</h3>

<p>Quando você quer compartilhar seu código com o mundo, é preciso pensar em como será descoberto e instalado. A seção <code>[project.urls]</code> ajuda nisso, assim como configurações de entrada do seu pacote.</p>

<pre><code class="language-toml">[project.urls]

Homepage = &quot;https://github.com/usuario/meu-projeto-incrivel&quot;

Documentation = &quot;https://meu-projeto.readthedocs.io&quot;

Repository = &quot;https://github.com/usuario/meu-projeto-incrivel.git&quot;

&quot;Bug Tracker&quot; = &quot;https://github.com/usuario/meu-projeto-incrivel/issues&quot;

Changelog = &quot;https://github.com/usuario/meu-projeto-incrivel/releases&quot;

[project.scripts]

meu-comando = &quot;meu_projeto.cli:main&quot;

[project.gui-scripts]

meu-app = &quot;meu_projeto.gui:launch&quot;</code></pre>

<p>O campo <code>[project.scripts]</code> define comandos CLI que será acessíveis depois da instalação. Se alguém instalar seu pacote, poderá simplesmente digitar <code>meu-comando</code> 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.</p>

<h3>Exemplo Prático: Arquivo Completo</h3>

<p>Aqui está um <code>pyproject.toml</code> realista e completo de um projeto médio:</p>

<pre><code class="language-toml">[build-system]

requires = [&quot;setuptools&gt;=61.0&quot;, &quot;wheel&quot;]

build-backend = &quot;setuptools.build_meta&quot;

[project]

name = &quot;data-pipeline&quot;

version = &quot;1.2.3&quot;

description = &quot;ETL simplificado para pipelines de dados&quot;

readme = &quot;README.md&quot;

requires-python = &quot;&gt;=3.9&quot;

license = {text = &quot;Apache-2.0&quot;}

authors = [

{name = &quot;Maria Dev&quot;, email = &quot;maria@company.com&quot;}

]

keywords = [&quot;etl&quot;, &quot;data&quot;, &quot;pipeline&quot;]

classifiers = [

&quot;Development Status :: 4 - Beta&quot;,

&quot;Intended Audience :: Developers&quot;,

&quot;License :: OSI Approved :: Apache Software License&quot;,

&quot;Programming Language :: Python :: 3.9&quot;,

&quot;Programming Language :: Python :: 3.10&quot;,

&quot;Programming Language :: Python :: 3.11&quot;,

]

dependencies = [

&quot;pandas&gt;=1.5.0&quot;,

&quot;sqlalchemy&gt;=2.0&quot;,

&quot;python-dotenv&gt;=0.20.0&quot;,

]

[project.optional-dependencies]

dev = [

&quot;pytest&gt;=7.0&quot;,

&quot;pytest-cov&gt;=4.0&quot;,

&quot;black&gt;=23.0&quot;,

&quot;ruff&gt;=0.1.0&quot;,

&quot;mypy&gt;=1.0&quot;,

]

docs = [

&quot;sphinx&gt;=5.0&quot;,

&quot;sphinx-rtd-theme&gt;=1.3&quot;,

]

postgres = [

&quot;psycopg2-binary&gt;=2.9&quot;,

]

[project.urls]

Homepage = &quot;https://github.com/company/data-pipeline&quot;

Documentation = &quot;https://data-pipeline.readthedocs.io&quot;

Repository = &quot;https://github.com/company/data-pipeline.git&quot;

&quot;Bug Tracker&quot; = &quot;https://github.com/company/data-pipeline/issues&quot;

[project.scripts]

pipeline = &quot;data_pipeline.cli:main&quot;

[tool.setuptools]

packages = [&quot;data_pipeline&quot;]

[tool.black]

line-length = 100

target-version = [&#039;py39&#039;, &#039;py310&#039;, &#039;py311&#039;]

[tool.ruff]

line-length = 100

target-version = &quot;py39&quot;

select = [&quot;E&quot;, &quot;W&quot;, &quot;F&quot;, &quot;I&quot;, &quot;C&quot;, &quot;B&quot;, &quot;UP&quot;]

[tool.mypy]

python_version = &quot;3.9&quot;

disallow_untyped_defs = true

check_untyped_defs = true

[tool.pytest.ini_options]

testpaths = [&quot;tests&quot;]

addopts = &quot;-ra -q&quot;

[tool.coverage.run]

source = [&quot;data_pipeline&quot;]

[tool.coverage.report]

exclude_lines = [

&quot;pragma: no cover&quot;,

&quot;if TYPE_CHECKING:&quot;,

]</code></pre>

<p>Esse arquivo é autossuficiente. Com apenas isso, alguém pode clonar seu repositório, rodar <code>pip install -e .[dev]</code>, e ter um ambiente pronto para desenvolvimento. Não há <code>setup.py</code> boilerplate, não há <code>requirements.txt</code> desincronizado, não há <code>setup.cfg</code> misterioso.</p>

<h2>Conclusão</h2>

<p>Três pontos críticos aprendidos nesta jornada:</p>

<ol>

<li><strong>Centralização é poder</strong>: O <code>pyproject.toml</code> é 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.</li>

</ol>

<ol>

<li><strong>Compatibilidade é intencional</strong>: Declarar explicitamente <code>requires-python</code>, usar seções <code>[project.optional-dependencies]</code> 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.</li>

</ol>

<ol>

<li><strong>Ferramentas modernas convertem em prática</strong>: 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.</li>

</ol>

<p>O <code>pyproject.toml</code> não é apenas um arquivo de configuração — é um contrato claro entre você e seus usuários/colaboradores.</p>

<h2>Referências</h2>

<ul>

<li><a href="https://www.python.org/dev/peps/pep-0517/" target="_blank" rel="noopener noreferrer">PEP 517 – A build-system independent format</a></li>

<li><a href="https://www.python.org/dev/peps/pep-0621/" target="_blank" rel="noopener noreferrer">PEP 621 – Storing project metadata in pyproject.toml</a></li>

<li><a href="https://toml.io/en/" target="_blank" rel="noopener noreferrer">TOML Documentation</a></li>

<li><a href="https://setuptools.pypa.io/en/latest/userguide/pyproject_config.html" target="_blank" rel="noopener noreferrer">Setuptools: Configuring setuptools using pyproject.toml</a></li>

<li><a href="https://hynek.me/articles/python-packaging/" target="_blank" rel="noopener noreferrer">Hynek Schlawack&#039;s Python Packaging Guide</a></li>

</ul>

<p>&lt;!-- FIM --&gt;</p>

Comentários

Mais em Python

Dominando Testes de APIs Python: pytest com httpx e TestClient do FastAPI em Projetos Reais
Dominando Testes de APIs Python: pytest com httpx e TestClient do FastAPI em Projetos Reais

Por que Testar APIs com Python? Testar uma API é tão importante quanto desenv...

O que Todo Dev Deve Saber sobre httpx e aiohttp em Python: Requisições HTTP Assíncronas
O que Todo Dev Deve Saber sobre httpx e aiohttp em Python: Requisições HTTP Assíncronas

Entendendo Programação Assíncrona em Python Antes de mergulharmos em e , prec...

Mocks em Python: unittest.mock, patch e pytest-mock na Prática: Do Básico ao Avançado
Mocks em Python: unittest.mock, patch e pytest-mock na Prática: Do Básico ao Avançado

Introdução: Por que Mocks são Essenciais Quando desenvolvemos software profis...