Cuando estructuras un proyecto real con múltiples paquetes y submódulos, tarde o temprano te encuentras con la pregunta: ¿from . import utils o from mypackage import utils? La diferencia no es estilística, es semántica, y equivocarse tiene consecuencias silenciosas que solo aparecen cuando cambias la forma de ejecutar el código.
Cómo resuelve Python cada import
Un import absoluto (import mypackage.module o from mypackage.module import something) siempre arranca desde la raíz: Python recorre sys.path de principio a fin buscando un paquete llamado mypackage. No importa desde qué archivo lo escribas, la ruta de búsqueda es la misma. Eso hace que el resultado sea predecible y estable.
Un import relativo (from . import module, from .. import something) no usa sys.path. En cambio, usa el atributo __package__ del módulo que ejecuta el import para calcular una ruta dentro de la jerarquía de paquetes. El punto . significa “el paquete en el que estoy ahora mismo”, .. significa “el paquete padre”, y así sucesivamente. Python traduce esos puntos a un nombre absoluto internamente y luego importa ese módulo, por lo que no estás evitando el sistema de importación, solo calculando la ruta de una manera diferente.
El problema clave: __package__ solo existe cuando Python carga un archivo como parte de un paquete. Cuando ejecutas un archivo directamente con python mi_modulo.py, Python lo trata como el script de entrada (__name__ == "__main__") y fija __package__ en None. En ese momento cualquier import relativo lanza ImportError: attempted relative import with no known parent package. El import relativo no es más “rápido” ni más directo, solo tiene un modo de fallo que el absoluto no tiene.
PEP 8 recomienda imports absolutos como norma general precisamente por esto: son inequívocos, funcionan desde cualquier punto de entrada, y cuando los lees en un code review entiendes al instante de dónde viene cada cosa. Los relativos tienen un caso de uso legítimo —paquetes internos grandes donde quieres que los módulos sean agnósticos de cómo se instaló el paquete externo— pero ese caso es más raro de lo que parece.
# Estructura del proyecto:
#
# ecommerce/
# ├── __init__.py
# ├── core/
# │ ├── __init__.py
# │ ├── models.py
# │ └── validators.py
# └── notifications/
# ├── __init__.py
# ├── email.py
# └── sms.py
# ─── ecommerce/core/models.py ───────────────────────────────────────────
# Import absoluto: inequívoco, funciona siempre.
from ecommerce.core.validators import validate_email
# Import relativo equivalente: solo válido cuando Python carga
# este archivo como parte del paquete ecommerce.core.
# from .validators import validate_email
class User:
def __init__(self, email: str) -> None:
self.email = validate_email(email)
def __repr__(self) -> str:
return f"User(email={self.email!r})"
# ─── ecommerce/core/validators.py ───────────────────────────────────────
import re
def validate_email(value: str) -> str:
if not re.match(r"[^@]+@[^@]+\.[^@]+", value):
raise ValueError(f"Email inválido: {value!r}")
return value.lower()
# ─── ecommerce/notifications/email.py ───────────────────────────────────
# Necesitamos User desde el paquete hermano `core`.
# Absoluto: la mejor opción aquí. Queda claro que User vive
# en ecommerce.core, no en este mismo paquete.
from ecommerce.core.models import User
# Con relativo necesitarías subir un nivel y luego entrar en core:
# from ..core.models import User
# Es válido, pero quien lee el código tiene que mentalmente
# resolver la jerarquía de puntos.
def notify(user: User, subject: str, body: str) -> None:
# Lógica de envío omitida; lo relevante es el import.
print(f"[EMAIL] To: {user.email} | Subject: {subject}")
print(f" {body}")
# ─── ecommerce/notifications/sms.py ─────────────────────────────────────
# Caso donde el relativo tiene sentido: importar desde el
# paquete inmediato cuando quieres que el submódulo no esté
# acoplado al nombre externo del paquete raíz.
# Útil si este subpaquete `notifications` pudiera distribuirse
# de forma independiente en el futuro.
from . import email as email_module # hermano dentro de notifications
from ecommerce.core.models import User # pero User sigue siendo absoluto
def notify_all(user: User, message: str) -> None:
email_module.notify(user, subject="Notificación", body=message)
print(f"[SMS] To: {user.email} | {message}")
Qué está pasando en cada decisión
En models.py el import de validators podría ser relativo (from .validators import validate_email) y funcionaría perfectamente mientras Python cargue el módulo como parte del paquete. Pero si alguien añade un test rápido al final del archivo y lo ejecuta directamente para probarlo, el import relativo romperá. El absoluto no cambia de comportamiento según cómo lo ejecutes.
En notifications/email.py la elección es aún más clara. from ecommerce.core.models import User le dice al lector exactamente dónde buscar User sin descifrar cuántos niveles sube ... El import relativo from ..core.models import User es correcto, pero obliga a quien lee a conocer la estructura del árbol de directorios para entender el origen de User.
notifications/sms.py muestra el caso donde el relativo tiene argumento defensivo real: from . import email expresa que email y sms son compañeros dentro del mismo subpaquete, y ese acoplamiento intencional es parte del diseño. Si renombraras el subpaquete notifications a alerts, el import relativo seguiría funcionando sin tocar nada. Con absoluto tendrías que actualizar from ecommerce.notifications import email. Ese es el trade-off honesto: los relativos son más robustos ante renombrado del paquete raíz, los absolutos son más robustos ante cambios en la jerarquía interna y ante distintos puntos de entrada.
Errores que debes conocer
Error: Usar import relativo en un script ejecutado directamente, lo que fija __package__ en None y hace imposible resolver el punto.
# ❌ Wrong — ejecutado como: python ecommerce/core/models.py from .validators import validate_email # ImportError inmediato # ✅ Right — ejecutado como: python -m ecommerce.core.models # o bien, simplemente usar el import absoluto en el módulo: from ecommerce.core.validators import validate_email
python -m instruye a Python a cargar el módulo dentro de su paquete, estableciendo __package__ correctamente. Pero es más fácil evitar el problema usando absolutos en cualquier módulo que pueda ejecutarse directamente.
Error: Crear un módulo local con el mismo nombre que uno de la biblioteca estándar y confundirse al importar.
# ❌ Wrong — tienes un archivo ecommerce/email.py propio # y dentro de él haces: import email # ¿estás importando tu módulo o el de la stdlib? # Con absolutos desde dentro del paquete, esto puede ser ambiguo # dependiendo de cómo está configurado sys.path. # ✅ Right — import explícito que no deja duda: import email as stdlib_email # stdlib from ecommerce import email as local # el tuyo
Cuando los nombres colisionan, el orden de sys.path decide quién gana, y ese orden puede diferir entre desarrollo y producción. Ser explícito en el nombre del import es la única defensa real.
N° 66