Escenarios de Importación Circular
Las importaciones circulares ocurren cuando el módulo A carga el módulo B mientras B aún se está inicializando e intenta importar A. Python genera ImportError o deja los nombres como marcadores de posición semivacíos tipo None.
Receta
Tarjeta de receta de referencia rápida, lista para copiar y pegar.
# Diagnosticar
python -c "import mypackage"
python -X importtime -c "import mypackage" 2>&1 | head
# Solución estructural: extraer tipos compartidos
# mypackage/domain/types.py <- ambos lados importan este módulo hojaCuándo usar esto:
ImportError: cannot import name 'X' from partially initialized module- Los tipos funcionan en tiempo de ejecución pero mypy/pyright informan importaciones circulares
- La adición de un nuevo módulo rompe el orden de inicio que "solía funcionar"
- Las importaciones perezosas se multiplican como mecanismo de adaptación
Ejemplo de Trabajo
# Diseño roto:
# orders/models.py importa billing/invoices.py
# billing/invoices.py importa orders/models.py
# Paso 1: reproducir
# $ python -c "import orders.models"
# ImportError: cannot import name 'Order' from partially initialized module 'orders.models'
# Paso 2: extraer tipos hoja
# shared/types.py
from dataclasses import dataclass
@dataclass(frozen=True)
class OrderId:
value: str
@dataclass(frozen=True)
class Money:
cents: int
# orders/models.py
from shared.types import Money, OrderId
@dataclass
class Order:
id: OrderId
total: Money
# billing/invoices.py
from shared.types import Money, OrderId
from orders.models import Order
def invoice_for(order: Order) -> Money:
return order.total# Puente temporal (documentar ticket para eliminar)
def get_order(order_id: str):
from orders.models import Order # importación perezosa dentro de la función
return Order(OrderId(order_id), Money(0))Lo que esto demuestra:
- Los tipos de valor compartidos se mueven a un módulo hoja que ambos lados importan.
- Los tipos de dominio importan hacia adentro; facturación importa
Ordersolo después de queorders.modelstermine de cargarse. - La importación perezosa desbloquea emergencias pero no reemplaza la estructura.
Análisis Profundo
Cómo Funciona
- La importación se ejecuta de arriba a abajo una vez por módulo; los módulos incompletos residen en
sys.modules. - El acceso a atributos durante la inicialización parcial falla o ve nombres faltantes.
- Las importaciones solo para verificación de tipos (
if TYPE_CHECKING:) rompen ciclos para herramientas estáticas sin costo en tiempo de ejecución. - Las reexportaciones del paquete
__init__.pypueden crear accidentalmente ciclos entre subpaquetes.
Formas Comunes de Ciclos
| Patrón | Síntoma | Solución |
|---|---|---|
| Importaciones mutuas A ↔ B | ImportError de inicio | módulo types hoja |
models ↔ services | nombre de clase faltante | capa de puertos/casos de uso |
Fábrica app ↔ blueprints | Fallo de inicialización de Flask/Django | registro tardío de blueprints |
Gráfico de reexportación de __init__ | Importación lenta/pesada | API pública estrecha __all__, perezosa |
Notas de Python
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from orders.models import Order
def describe(order: "Order") -> str:
return "order"Trampas
- Solo anotaciones de cadena - las comillas ocultan ciclos sin arreglar la arquitectura. Solución: extraer tipos o invertir la dependencia.
__init__.py"gordo" - importar un subpaquete ejecuta todo el árbol. Solución:__init__.pyvacío o mínimo; importaciones explícitas de submódulos.- Efectos secundarios en la importación - la conexión a la base de datos al importar empeora la depuración de ciclos. Solución: mover IO a
main/hooks de inicio. - La recopilación de pytest importa todo - los ciclos se ocultan hasta que se ejecuta toda la suite. Solución: prueba de humo
import packageen CI. - Renombrar sin invertir aristas - el ciclo regresa con nuevos nombres de módulo. Solución: regla de dependencia: el dominio nunca importa adaptadores.
- Importaciones circulares de
conftestde prueba -conftest.pyimporta la aplicación que importa ayudantes de prueba. Solución: fixtures entests/supportdedicado sin importar módulos de aplicación mutuamente.
Alternativas
| Alternativa | Usar Cuando | No Usar Cuando |
|---|---|---|
Módulo hoja de types/protocols | Formas de datos compartidas | El comportamiento aún se llama mutuamente |
| Fusionar módulos temporalmente | Dos archivos pequeños entrelazados | Dominios grandes - crea un módulo dios |
Importaciones TYPE_CHECKING | Referencias inversas solo de tipado | Necesidades de tiempo de ejecución de clase real |
| Importaciones de funciones perezosas | Solución de emergencia con ticket | Diseño permanente |
Preguntas Frecuentes
¿Cómo veo la cadena de importación?
Ejecuta python -X importtime -c "import pkg" y lee los bordes más lentos/cíclicos. Herramientas como pydeps grafican importaciones estáticamente.
¿`from __future__ import annotations` arregla los ciclos en tiempo de ejecución?
Pospone la evaluación de anotaciones (el comportamiento de PEP 563 varía según la versión) pero no arregla las importaciones mutuas en tiempo de ejecución de clases y funciones.
¿Dónde deben vivir los puertos de Protocol?
ports/ o domain/protocols.py - importado por adaptadores y casos de uso, no por frameworks que se importan entre sí.
¿Pueden funcionar a veces las importaciones circulares?
Accidentalmente, si un módulo solo necesita el otro después de que ambos terminen de inicializarse. Depende del orden de importación y se rompe al refactorizar.
¿Cómo crean ciclos los routers de FastAPI?
Los routers importan servicios que importan app para Depends. Solución: fábricas de dependencias en un módulo wiring importado después de la creación de app.
¿Qué pasa con los modelos de Django que se importan entre sí?
Usa referencias de cadena 'other.Model' en ForeignKey y mueve los enums compartidos al módulo hoja models/enums.py.
¿Debería usar `importlib` en código de producción?
Rara vez. Las importaciones dinámicas oscurecen los gráficos. Prefiere soluciones estructurales; usa importlib solo para plugins con puntos de entrada explícitos.
¿Cómo ayuda la estructura `src`?
Fuerza nombres de paquete explícitos y detecta importaciones relativas erróneas que empeoran los ciclos a través de rutas de módulo duplicadas.
¿Puede `ruff` detectar ciclos?
Las reglas de lint de ruff detectan algunos problemas de importación; los contratos de import-linter fuerzan la dirección de la capa en CI.
¿Cuándo es correcta la fusión de módulos?
Cuando dos módulos tienen <200 líneas, siempre cambian juntos y representan un único concepto delimitado. Documenta la fusión en un ADR.
¿Cómo prevengo regresiones?
Agrega python -c "import myservice" a CI y contratos opcionales de import-linter entre domain, use_cases, adapters.
¿`TYPE_CHECKING` afecta el rendimiento en tiempo de ejecución?
No. Las importaciones bajo if TYPE_CHECKING: son eliminadas en tiempo de ejecución solo por los verificadores de tipos; no se ejecutan durante las importaciones normales.
Relacionado
- Decisiones de Refactorización - soluciones de ciclos clasificadas
- Estructura de
srcy Paquetes - higiene de paquetes - Arquitectura Limpia / Hexagonal - dirección de dependencia
- Herramientas de Depuración - inspeccionar el estado de importación
- Conflictos de Dependencia y Entorno - paquete incorrecto en la ruta imita ciclos
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+.