Capítulo 49, Avançado
Empacotamento e estrutura de projeto
Transformar código em algo instalável, versionável e publicável. Mesmo que você nunca publique no PyPI, a estrutura de pacote resolve problemas de importação e de testes.
Código deste capítulo: avancado/cap49_empacotamento.py
Por que usar o layout src
Colocar o pacote dentro de uma pasta src/ evita um erro sutil: com o código na raiz, os testes importam a pasta local por acidente e passam mesmo que o pacote instalado esteja quebrado. Com src/, a única forma de importar é instalando o pacote, e então você testa o que o usuário vai receber.
exemplos/pacote/
pyproject.toml
README.md
src/
calc_notes/
__init__.py
operacoes.py
cli.py
tests/
test_operacoes.py
O pyproject.toml
Tudo vive em um arquivo. A seção [build-system] diz como construir. A seção [project] descreve o pacote. E [project.scripts] cria um comando de terminal ligado a uma função:
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "calc-notes"
version = "0.1.0"
description = "Calculadora de exemplo do livro Python na Prática"
readme = "README.md"
requires-python = ">=3.12"
dependencies = []
[project.scripts]
calc-notes = "calc_notes.cli:main"
[dependency-groups]
dev = ["pytest>=8"]
O código do pacote
def somar(a: float, b: float) -> float:
return a + b
def dividir(a: float, b: float) -> float:
if b == 0:
raise ZeroDivisionError("divisor não pode ser zero")
return a / b
"""Pacote de exemplo do livro Python na Prática."""
from importlib.metadata import PackageNotFoundError, version
from calc_notes.operacoes import dividir, somar
try:
__version__ = version("calc-notes")
except PackageNotFoundError:
__version__ = "0+local"
__all__ = ["__version__", "dividir", "somar"]
Eu leio a versão dos metadados do pacote instalado em vez de repeti-la no código. Assim existe uma só fonte da verdade, o pyproject.toml.
import argparse
from calc_notes.operacoes import somar
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(prog="calc-notes")
parser.add_argument("a", type=float)
parser.add_argument("b", type=float)
args = parser.parse_args(argv)
print(somar(args.a, args.b))
return 0
import pytest
from calc_notes import dividir, somar
def test_somar():
assert somar(2, 3) == 5
def test_dividir_por_zero():
with pytest.raises(ZeroDivisionError):
dividir(1, 0)
calc-notes
Calculadora de exemplo do livro Python na Prática.
Instalar em modo editável e usar
O modo editável instala o pacote apontando para o seu código-fonte, então cada alteração vale na hora, sem reinstalar:
cd exemplos/pacote
uv pip install -e .
calc-notes 2 3
5.0
Sem o uv, dentro de um ambiente virtual: python -m pip install -e .. Para testar, rode o pytest dentro da pasta do pacote.
O módulo importlib.metadata consulta os metadados de qualquer pacote instalado, e levanta PackageNotFoundError se ele não existir:
from importlib.metadata import PackageNotFoundError, version
try:
print(version("pacote-que-nao-existe"))
except PackageNotFoundError as erro:
print("não instalado:", erro)
não instalado: No package metadata was found for pacote-que-nao-existe
Construir e publicar
Um pacote é distribuído em dois formatos: o sdist (código-fonte, .tar.gz) e o wheel (já pronto para instalar, .whl). Qualquer um dos comandos abaixo gera os dois na pasta dist/:
uv build
Building source distribution...
Building wheel from source distribution...
Successfully built dist/calc_notes-0.1.0.tar.gz
Successfully built dist/calc_notes-0.1.0-py3-none-any.whl
Alternativas equivalentes: python -m build (com o pacote build) e poetry build. Para publicar, treine primeiro no TestPyPI, um índice de ensaio:
uv publish --publish-url https://test.pypi.org/legacy/ --token "$TOKEN"
Também funciona o twine upload --repository testpypi dist/*. Em projetos reais, eu prefiro a publicação confiável (trusted publishing): o GitHub Actions prova a sua identidade ao PyPI por um token temporário, e você nunca guarda uma senha de longa duração.
Escolher o backend de construção
| Backend | Quando usar |
|---|---|
hatchling | Escolha padrão para pacotes em Python puro, simples e configurável |
uv_build | Mesmo cenário, muito rápido, integrado ao uv |
setuptools | Projetos antigos, ou os que têm extensões em C |
poetry-core | Projetos gerenciados pelo Poetry |
maturin, scikit-build-core | Extensões em Rust ou C++ |
Bibliotecas e aplicações pensam diferente sobre versões
Uma aplicação é instalada em um ambiente que você controla. Faça commit do lockfile e fixe as versões. Uma biblioteca é instalada no ambiente de outras pessoas, junto com outras bibliotecas. Declare intervalos amplos (requests>=2.32,<3), porque versões exatas causam conflito para quem usa a sua. O lockfile da biblioteca serve só para o seu desenvolvimento e para os seus testes.
Exercício 1
Executar com python -m
Faça o pacote rodar com python -m calc_notes 2 3, adicionando um arquivo ao pacote.