Estructura de Paquetes y Diseño de src/
El diseño src/ instala tu paquete antes de que se ejecuten las pruebas, de modo que las importaciones coincidan con la producción y los hacks accidentales de rutas "funciona en mi portátil" fallen en CI.
Receta
Tarjeta de receta de referencia rápida, lista para copiar y pegar.
# pyproject.toml (extracto)
[project]
name = "billing"
version = "0.1.0"
requires-python = ">=3.14"
[tool.setuptools.packages.find]
where = ["src"]
# src/billing/__init__.py
"""Paquete del dominio de facturación."""
# tests/test_invoice.py
from billing.invoices import compute_total
def test_compute_total() -> None:
assert compute_total([10, 20]) == 30Cuándo usar esto:
- Cualquier biblioteca o servicio instalable empaquetado para Docker/CI
- Equipos que experimentan errores de importación solo en CI o después de
pip install - Proyectos que separan puntos de entrada de dominio, adaptadores y CLI
- Monorepos con múltiples paquetes bajo un mismo repositorio
Ejemplo de Trabajo
# 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()Lo que esto demuestra:
- El código del paquete vive bajo
src/orders/, las pruebas importanorderscomo un nombre instalado - El dominio (
models,totals) se mantiene libre de detalles de CLI y persistencia project.scriptsexpone un punto de entrada de consola establerequires-pythondocumenta el límite 3.14 desde el primer día
Análisis Profundo
Cómo Funciona
pip install -e .(ouv sync) colocasrc/ordersensys.pathcomoordersde nivel superior- Las pruebas y las aplicaciones importan
orders.domain, nosrc.orders.domain - Las instalaciones editables releen los cambios de origen sin reinstalar
- La CI que solo configura
PYTHONPATH=.oculta metadatos de empaquetado faltantes
Árbol Recomendado
my-service/
pyproject.toml
src/
my_service/
__init__.py
domain/
adapters/
api/ # Routers de FastAPI/Flask
tests/
test_domain.py
README.md
Comandos de Empaquetado
| Tarea | Comando |
|---|---|
| Instalación editable | uv sync o pip install -e . |
| Ejecutar pruebas | pytest (después de la instalación) |
| Script de consola | orders-api después de la instalación |
Notas de Python
# Evitar colisiones de nombres de paquetes/módulos
# MAL: src/orders.py Y paquete src/orders/
# BIEN: un directorio de paquete con submódulos
# API pública explícita en __init__.py
from orders.domain.totals import order_total as order_total
__all__ = ["order_total"]Trampas Comunes
- Probar sin instalar -
pytestfunciona localmente porque la raíz del repositorio está ensys.path, luego falla en Docker. Solución: siemprepip install -e .en CI antes de las pruebas. - Nombres de paquete duplicados - un archivo
orders.pyjunto aorders/oculta el paquete. Solución: una convención de diseño; eliminar módulos de nivel superior extraños. - Importaciones relativas implícitas en scripts -
from models import Xfalla cuando se ejecuta como módulo. Solución: importaciones absolutas desde la raíz del paquete (from orders.domain.models import X). - Falta
__init__.py- los paquetes de namespace pueden funcionar pero confunden las herramientas y las exportaciones explícitas. Solución: agregar__init__.pya menos que necesites deliberadamente namespaces PEP 420. - Anidación profunda sin propósito -
src/foo/bar/baz/quxcon una función por archivo. Solución: aplanar hasta que una carpeta tenga un límite claro (dominio, api, adaptadores). - Pruebas dentro de
src/- enviadas accidentalmente en wheels. Solución: mantener las pruebas entests/de nivel superior.
Alternativas
| Alternativa | Usar Cuando | No Usar Cuando |
|---|---|---|
| Diseño plano (paquete en la raíz del repositorio) | Scripts de un solo archivo, notebooks | Bibliotecas y servicios instalados en CI |
Paquetes de namespace (sin __init__.py) | Dividir subpaquetes entre repositorios | Equipos pequeños que desean herramientas simples |
Árbol packages/ de Monorepo | Múltiples bibliotecas publicables en un solo repositorio | Un servicio con un solo paquete |
Preguntas Frecuentes
¿Por qué el diseño src es mejor que un directorio de paquetes plano?
Los diseños planos permiten que la raíz del repositorio satisfaga las importaciones durante el desarrollo, incluso cuando los metadatos del paquete son incorrectos. El diseño src fuerza la instalación, revelando las entradas faltantes de pyproject.toml antes de producción.
¿Necesito src/ para una aplicación FastAPI que nunca se publica en PyPI?
Sí. Las imágenes Docker y la CI todavía instalan la aplicación como un paquete. El diseño src mantiene uvicorn orders.api:app consistente en todas partes.
¿Dónde viven las migraciones de Alembic?
Mantén las migraciones en la raíz del repositorio (alembic/) o bajo src/orders/adapters/db/migrations/ - pero importa los modelos a través del nombre del paquete instalado, no de rutas de archivo relativas.
¿Cómo estructuro extras opcionales?
Declara grupos de dependencias opcionales en pyproject.toml ([project.optional-dependencies]) e importa perezosamente adaptadores pesados (por ejemplo, PyTorch) dentro de funciones.
¿Pueden las pruebas importar desde tests.helpers?
Sí. Mantén tests/ como un hermano de src/, nunca dentro del paquete. Las utilidades de prueba compartidas van a tests/helpers/ sin enviarlas.
¿Qué va en __all__?
Exporta solo la API pública estable. Los módulos internos permanecen sin documentar en __all__ para que las refactorizaciones no rompan a los llamadores externos.
¿Cómo se relacionan los workspaces de uv con el diseño src?
Cada miembro del workspace obtiene su propio árbol src/<package>/ y pyproject.toml. La raíz del workspace coordina los archivos de bloqueo compartidos con uv 0.6+.
¿Deben el dominio y los adaptadores ser paquetes instalables separados?
Comienza como subpaquetes (orders.domain, orders.adapters). Divide en distribuciones separadas solo cuando otro servicio necesite el dominio sin adaptadores.
¿Cómo ejecuto un módulo sin un script de consola?
python -m orders.cli funciona cuando el paquete está instalado y cli.py define un main o un guard del cuerpo del módulo.
¿Necesita ruff una configuración src especial?
Apunta ruff a src/ y tests/ por separado. src = ["src", "tests"] en pyproject.toml mantiene la ordenación de importaciones alineada con el nombre del paquete instalado.
¿Qué pasa si el código de terceros espera la raíz del repositorio en PYTHONPATH?
Envuelve scripts heredados en un punto de entrada de consola delgado que importe el paquete instalado. Deprecia los hacks de PYTHONPATH en README y CI.
¿Cuántos paquetes de nivel superior pertenecen a un solo repositorio?
Un servicio desplegable: un paquete principal. Múltiples paquetes: usa un workspace con propiedad clara por src/<name>/.
Relacionado
- Lista de Verificación de Decisiones de Arquitectura de Proyecto - decisión de diseño en contexto
- Arquitectura Limpia / Hexagonal - carpetas de dominio vs adaptadores
- Inyección de Dependencias y Cableado - cableado de módulos en la raíz de composición
- Configuración y Ajustes - ubicación del módulo de configuración
- Escenarios de Importación Circular - ciclos de gráficos de paquetes incorrectos
Versiones de Stack: Esta página fue escrita para Python 3.14.0 (estable 3.14, mantenimiento 3.13), FastAPI 0.115+, Django 5.2, Flask 3.1, Pydantic 2, PyTorch 2.6+, pandas 2.2+, Polars 1.x, ruff 0.9+, y uv 0.6+.