Estrutura de Layout e Pacotes src
O layout src/ instala seu pacote antes que os testes sejam executados, de modo que as importações correspondam à produção e os hacks acidentais de caminho "funciona no meu laptop" falhem no CI.
Receita
Cartão de receita de referência rápida - pronto para copiar e colar.
# pyproject.toml (excerto)
[project]
name = "billing"
version = "0.1.0"
requires-python = ">=3.14"
[tool.setuptools.packages.find]
where = ["src"]
# src/billing/__init__.py
"""Pacote de domínio de faturamento."""
# tests/test_invoice.py
from billing.invoices import compute_total
def test_compute_total() -> None:
assert compute_total([10, 20]) == 30Quando usar isso:
- Qualquer biblioteca instalável ou serviço empacotado para Docker/CI
- Equipes que enfrentam erros de importação apenas no CI ou após
pip install - Projetos que dividem pontos de entrada de domínio, adaptadores e CLI
- Monorepos com vários pacotes sob um único repositório
Exemplo de Trabalho
# pyproject.toml
[build-system]
requires = ["setuptools>=75"]
build-backend = "setuptools.build_meta"
[project]
name = "orders"
version = "0.1.0"
requires-python = ">=3.14"
dependencies = ["pydantic>=2"]
[project.scripts]
orders-api = "orders.cli:main"
[tool.setuptools.packages.find]
where = ["src"]
# src/orders/__init__.py
__all__ = ["domain", "adapters"]
# src/orders/domain/models.py
from dataclasses import dataclass
@dataclass(frozen=True)
class LineItem:
sku: str
quantity: int
unit_price_cents: int
# src/orders/domain/totals.py
from orders.domain.models import LineItem
def order_total(items: list[LineItem]) -> int:
return sum(i.quantity * i.unit_price_cents for i in items)
# src/orders/adapters/memory_repo.py
from orders.domain.models import LineItem
class InMemoryOrderRepo:
def __init__(self) -> None:
self._items: list[LineItem] = []
def add(self, item: LineItem) -> None:
self._items.append(item)
def list_items(self) -> list[LineItem]:
return list(self._items)
# src/orders/cli.py
from orders.adapters.memory_repo import InMemoryOrderRepo
from orders.domain.models import LineItem
from orders.domain.totals import order_total
def main() -> None:
repo = InMemoryOrderRepo()
repo.add(LineItem("ABC", 2, 1500))
print(order_total(repo.list_items()))
if __name__ == "__main__":
main()O que isso demonstra:
- O código do pacote vive sob
src/orders/, os testes importamorderscomo um nome instalado - Domínio (
models,totals) permanece livre de detalhes de CLI e persistência project.scriptsexpõe um ponto de entrada de console estávelrequires-pythondocumenta o limite 3.14 desde o primeiro dia
Análise Profunda
Como Funciona
pip install -e .(ouuv sync) colocasrc/src/ordersnosys.pathcomoordersde nível superior- Testes e aplicativos importam
orders.domain, nãosrc.orders.domain - Instalações editáveis relêem alterações de origem sem reinstalar
- CI que apenas define
PYTHONPATH=.oculta metadados de empacotamento ausentes
Árvore Recomendada
my-service/
pyproject.toml
src/
my_service/
__init__.py
domain/
adapters/
api/ # Roteadores FastAPI/Flask
tests/
test_domain.py
README.md
Comandos de Empacotamento
| Tarefa | Comando |
|---|---|
| Instalação editável | uv sync ou pip install -e . |
| Executar testes | pytest (após a instalação) |
| Script de console | orders-api após a instalação |
Notas de Python
# Evitar colisões de nomes de pacotes/módulos
# RUIM: src/orders.py E pacote src/orders/
# BOM: um diretório de pacote com submódulos
# API pública explícita em __init__.py
from orders.domain.totals import order_total as order_total
__all__ = ["order_total"]Armadilhas
- Testar sem instalar -
pytestfunciona localmente porque a raiz do repositório está nosys.path, e falha no Docker. Correção: sempre executepip install -e .no CI antes dos testes. - Nomes de pacotes duplicados - um arquivo
orders.pyao lado deorders/ofusca o pacote. Correção: uma convenção de layout; exclua módulos de nível superior órfãos. - Importações relativas implícitas em scripts -
from models import Xfalha quando executado como um módulo. Correção: importações absolutas da raiz do pacote (from orders.domain.models import X). __init__.pyausente - pacotes de namespace podem funcionar, mas confundem ferramentas e exportações explícitas. Correção: adicione__init__.py, a menos que você precise deliberadamente de namespaces PEP 420.- Aninhamento profundo sem propósito -
src/foo/bar/baz/quxcom uma função por arquivo. Correção: achate até que uma pasta tenha um limite claro (domínio, api, adaptadores). - Testes dentro de
src/- enviados acidentalmente em wheels. Correção: mantenha os testes emtests/de nível superior.
Alternativas
| Alternativa | Usar Quando | Não Usar Quando |
|---|---|---|
| Layout plano (pacote na raiz do repositório) | Scripts de arquivo único, notebooks | Bibliotecas e serviços instalados no CI |
Pacotes de namespace (sem __init__.py) | Dividir subpacotes entre repositórios | Equipes pequenas que desejam ferramentas simples |
Árvore packages/ de Monorepo | Várias bibliotecas publicáveis em um único repositório | Um serviço com um único pacote |
FAQs
Por que o layout src é melhor do que um diretório de pacotes plano?
Layouts planos permitem que a raiz do repositório satisfaça importações durante o desenvolvimento, mesmo quando os metadados de empacotamento estão incorretos. O layout src força a instalação, revelando entradas pyproject.toml ausentes antes da produção.
Preciso de src/ para um aplicativo FastAPI que nunca é publicado no PyPI?
Sim. Imagens Docker e CI ainda instalam o aplicativo como um pacote. O layout src mantém uvicorn orders.api:app consistente em todos os lugares.
Onde as migrações Alembic residem?
Mantenha as migrações na raiz do repositório (alembic/) ou em src/orders/adapters/db/migrations/ - mas importe modelos através do nome do pacote instalado, não de caminhos de arquivo relativos.
Como estruturar extras opcionais?
Declare grupos de dependências opcionais em pyproject.toml ([project.optional-dependencies]) e importe preguiçosamente adaptadores pesados (por exemplo, PyTorch) dentro de funções.
Os testes podem importar de tests.helpers?
Sim. Mantenha tests/ como um irmão de src/, nunca dentro do pacote. Utilitários de teste compartilhados vão para tests/helpers/ sem enviá-los.
O que vai em __all__?
Exporte apenas a API pública estável. Módulos internos permanecem indocumentados em __all__ para que refatorações não quebrem chamadores externos.
Como uv workspaces se relacionam com o layout src?
Cada membro do workspace recebe sua própria árvore src/<package>/ e pyproject.toml. A raiz do workspace coordena lockfiles compartilhados com uv 0.6+.
Domínio e adaptadores devem ser pacotes instaláveis separados?
Comece como subpacotes (orders.domain, orders.adapters). Divida em distribuições separadas apenas quando outro serviço precisar do domínio sem adaptadores.
Como executar um módulo sem um script de console?
python -m orders.cli funciona quando o pacote está instalado e cli.py define um main ou um guarda de corpo de módulo.
O ruff precisa de configuração src especial?
Aponte o ruff para src/ e tests/ separadamente. src = ["src", "tests"] em pyproject.toml mantém a ordenação de importação alinhada com o nome do pacote instalado.
E se o código de terceiros esperar a raiz do repositório no PYTHONPATH?
Envolva scripts legados em um ponto de entrada de console fino que importe o pacote instalado. Deprecie hacks de PYTHONPATH no README e CI.
Quantos pacotes de nível superior pertencem a um único repositório?
Um serviço implantável: um pacote principal. Múltiplos pacotes: use um workspace com propriedade clara por src/<nome>/.
Relacionado
- Checklist de Decisão de Arquitetura de Projeto - decisão de layout em contexto
- Arquitetura Limpa / Hexagonal - pastas de domínio vs adaptadores
- Injeção de Dependência e Conexão - conectando módulos na raiz de composição
- Configuração e Ajustes - posicionamento do módulo de configurações
- Cenários de Importação Circular - ciclos de gráficos de pacotes ruins
Versões de Stack: Esta página foi escrita para Python 3.14.0 (3.14 estável, 3.13 de manutenção), FastAPI 0.115+, Django 5.2, Flask 3.1, Pydantic 2, PyTorch 2.6+, pandas 2.2+, Polars 1.x, ruff 0.9+, e uv 0.6+.