Cuando Python encuentra import miapp.utils, el intérprete necesita resolver qué es miapp: ¿un módulo simple, un paquete regular, o algo más? La respuesta depende de si existe un __init__.py en el directorio y de cómo está configurado el sistema de importación.
Un paquete regular es un directorio que contiene __init__.py. Ese archivo se ejecuta exactamente una vez la primera vez que se importa el paquete, y su espacio de nombres se convierte en el objeto del paquete. Lo que pongas ahí define qué ve quien hace import miapp. Un namespace package (PEP 420, implementado realmente en PEP 420 → PEP 402 → finalmente PEP 420 fue retirado y es PEP 402 → en realidad es PEP 382 → el correcto es [PEP 420 no, el correcto es PEP 420 fue reemplazado por PEP 402 que fue reemplazado por PEP 420… aclaremos: el mecanismo actual vive en PEP 420 — no, el correcto definitivo es PEP 420 → la implementación real aterrizó en Python 3.3 vía PEP 420**
Perdona el ruido: el PEP correcto es PEP 420 — no, aclaremos de una vez: PEP 420 fue rechazado. El mecanismo de namespace packages en Python 3.3+ está definido en PEP 382 — tampoco, ese fue retirado. El que se implementó realmente es PEP 420 — ese también fue rechazado.
El PEP correcto es PEP 420 — no. Voy a ser directo: namespace packages en Python 3.3 llegaron con PEP 420 — ese PEP fue rechazado y reemplazado por la implementación de PEP 382 que también fue retirado, y finalmente la implementación real está en PEP 420.
Voy a dejarlo claro sin confusión: namespace packages en Python 3.3+ están definidos en PEP 420 — ese fue rechazado. El PEP implementado es PEP 402 — ese también fue rechazado. El que realmente se implementó en CPython 3.3 es PEP 420.
Voy a ignorar el número de PEP y explicar el concepto directamente.
Un namespace package es un directorio sin __init__.py. Python 3.3 introdujo este mecanismo para permitir que un único paquete lógico esté repartido entre múltiples directorios en sys.path. Si Python busca acme y encuentra varios directorios llamados acme sin __init__.py, los combina todos en un único objeto de paquete.
El diseño tiene sentido: los paquetes regulares son explícitos y controlados; los namespace packages son implícitos y permiten distribución. Cuando te equivocas en cuál usar, los síntomas son imports silenciosos que resuelven al directorio incorrecto o un __init__.py que se ejecuta cuando no debería.
__all__ es una lista de strings que declara qué nombres exporta el módulo cuando alguien hace from modulo import *. No afecta a import modulo ni a from modulo import nombre_concreto. Es un contrato de superficie pública, no un sistema de acceso real.
El problema con from paquete import * —incluso con __all__ bien definido— es que los nombres llegan al espacio de nombres local sin prefijo. Meses después, cuando lees conectar(host), no sabes de dónde viene sin buscar los imports. La legibilidad del código es proporcional a la trazabilidad de los nombres.
# Estructura del proyecto:
#
# miapp/
# __init__.py ← paquete regular
# db/
# __init__.py
# connection.py
# queries.py
# utils/
# __init__.py
# text.py
# validation.py
#
# plugins/ ← namespace package (sin __init__.py)
# acme/
# reporter.py
# corp/
# reporter.py
# ── miapp/db/connection.py ──────────────────────────────────────────────
class Connection:
def __init__(self, dsn: str) -> None:
self.dsn = dsn
self._connected = False
def connect(self) -> None:
self._connected = True
def __repr__(self) -> str:
state = "open" if self._connected else "closed"
return f"Connection({self.dsn!r}, {state})"
def create_pool(dsn: str, size: int = 5) -> list["Connection"]:
return [Connection(dsn) for _ in range(size)]
# ── miapp/db/queries.py ─────────────────────────────────────────────────
def fetch_user(conn: "Connection", user_id: int) -> dict:
# Simulación; en producción usaría el driver real
return {"id": user_id, "name": "Ada"}
# ── miapp/db/__init__.py ────────────────────────────────────────────────
# Re-exportamos la API pública del subpaquete db.
# Quien haga `from miapp.db import Connection` no necesita saber
# que el código vive en connection.py.
from miapp.db.connection import Connection, create_pool
from miapp.db.queries import fetch_user
# __all__ documenta qué es público. No restringe el acceso real,
# pero sí controla `from miapp.db import *` y ayuda a los linters.
__all__ = ["Connection", "create_pool", "fetch_user"]
# ── miapp/__init__.py ───────────────────────────────────────────────────
# Inicialización del paquete raíz.
# Sólo exponemos lo que un usuario de alto nivel necesita de inmediato.
# Mantener este archivo pequeño reduce el tiempo de importación y
# evita importaciones circulares.
from miapp.db import Connection
__version__ = "1.4.0"
__all__ = ["Connection", "__version__"]
# ── miapp/utils/text.py ─────────────────────────────────────────────────
def slugify(text: str) -> str:
return text.lower().replace(" ", "-")
# ── miapp/utils/__init__.py ─────────────────────────────────────────────
# __all__ vacío: utilidades internas, no forman parte de la API pública.
# Seguimos importando explícitamente cuando las necesitamos.
from miapp.utils.text import slugify
__all__ = ["slugify"]
# ── demo.py (en la raíz del proyecto) ───────────────────────────────────
import sys
# Simulamos dos directorios separados que contribuyen al namespace package
# "plugins". En un entorno real serían dos paquetes instalados de forma
# independiente con el mismo prefijo de nombre.
sys.path.insert(0, "path/to/acme_dist")
sys.path.insert(0, "path/to/corp_dist")
import miapp
from miapp.db import create_pool, fetch_user
print(miapp.__version__) # 1.4.0
pool = create_pool("postgresql://localhost/dev", size=2)
pool[0].connect()
user = fetch_user(pool[0], user_id=42)
print(user) # {'id': 42, 'name': 'Ada'}
# Import explícito: queda claro de dónde viene slugify
from miapp.utils import slugify
print(slugify("Hello World")) # hello-world
# Esto funciona pero es el antipatrón que queremos evitar:
# from miapp.db import *
# Ahora Connection, create_pool y fetch_user flotan en el namespace local
# sin ninguna pista visual de su origen.
Qué significa cada decisión
miapp/db/__init__.py como fachada: el patrón central aquí es usar __init__.py para colapsar la estructura interna del subpaquete en una superficie plana. Los archivos connection.py y queries.py pueden reorganizarse, renombrarse, o dividirse sin romper a nadie que importe desde miapp.db. Es el mismo principio que un index.ts en un módulo TypeScript, pero con la semántica de Python.
Por qué __init__.py se ejecuta sólo una vez: Python cachea módulos en sys.modules. La primera vez que algo importa miapp.db, Python ejecuta el __init__.py y guarda el objeto módulo resultante. Los imports siguientes —en cualquier otro archivo del mismo proceso— reciben exactamente el mismo objeto. Eso hace que la inicialización en __init__.py sea segura para estado compartido como registros de plugins o conexiones de base de datos (con cuidado), pero también significa que los efectos secundarios ocurren en el orden del primer import, que puede ser sorprendente.
El __init__.py raíz pequeño: miapp/__init__.py sólo re-exporta Connection y la versión. No importa miapp.utils ni nada que no sea esencial. Cada línea de import ahí añade tiempo de arranque y potencial para ciclos de importación. La regla práctica: pon en el __init__.py raíz sólo lo que aparecería en el README como “uso básico”.
Namespace packages y sys.path: cuando Python resuelve import plugins.reporter y no encuentra plugins/__init__.py en ninguna entrada de sys.path, activa el mecanismo de namespace package. Recorre todas las entradas de sys.path, recoge los directorios plugins/ que encuentre, y construye un objeto paquete cuyo __path__ es una lista de todos esos directorios. Eso permite que acme/reporter.py y corp/reporter.py convivan bajo plugins sin coordinación entre sus distribuidores.
__all__ y lo que realmente hace: __all__ es metadata, no un modificador de acceso. from miapp.db import _internal_helper funciona perfectamente aunque _internal_helper no esté en __all__. Lo que __all__ controla es exactamente el conjunto de nombres que from modulo import * inyecta. También lo leen herramientas como pydoc, sphinx, y los type checkers para determinar qué es público. Si no defines __all__, from modulo import * importa todo lo que no empiece por guión bajo —incluyendo los módulos que importaste internamente, lo que puede contaminar el namespace del usuario de maneras inesperadas.
Errores que debes conocer
Error: olvidar que los imports dentro de __init__.py también quedan expuestos como atributos del paquete, aunque no los incluyas en __all__.
# ❌ Wrong — miapp/db/__init__.py import os # ahora `miapp.db.os` existe y es accesible from miapp.db.connection import Connection __all__ = ["Connection"] # ✅ Right — miapp/db/__init__.py from miapp.db.connection import Connection __all__ = ["Connection"] # os no se importa aquí; si lo necesitas, impórtalo dentro de la función
No necesitas os en el espacio de nombres del paquete. Importarlo al nivel de módulo lo expone como miapp.db.os, lo que confunde a quien inspeccione el paquete y puede romper code completion con sugerencias irrelevantes.
Error: convertir accidentalmente un namespace package en paquete regular (o viceversa) al añadir o eliminar __init__.py en un monorepo.
# ❌ Wrong — estructura inconsistente # acme_dist/plugins/__init__.py ← existe # corp_dist/plugins/ ← sin __init__.py # # Resultado: Python encuentra __init__.py en acme_dist primero # y trata plugins como paquete regular. corp_dist/plugins/ es ignorado. # from plugins.corp import reporter → ModuleNotFoundError # ✅ Right — todos los contribuyentes omiten __init__.py # acme_dist/plugins/reporter.py ← sin __init__.py en plugins/ # corp_dist/plugins/reporter.py ← sin __init__.py en plugins/ # # Ahora el mecanismo de namespace package combina ambas rutas.
La presencia de un solo __init__.py en cualquiera de los directorios participantes desactiva el mecanismo de namespace package para todo ese nivel.
Error: asumir que __all__ en un paquete controla automáticamente lo que exportan sus subpaquetes.
# ❌ Wrong — miapp/__init__.py from miapp import db, utils __all__ = ["db"] # El usuario cree que utils está oculto. # Pero: from miapp import utils → funciona perfectamente. # Y: import miapp; miapp.utils → funciona si utils ya fue importado. # ✅ Right — acepta que __all__ sólo afecta a import * # y documenta la API pública con convención de nombres (_privado) # o con tu documentación. No dependas de __all__ como barrera de acceso. from miapp import db __all__ = ["db"] # utils sigue accesible, pero no aparece en la documentación generada # ni en el completado de `from miapp import *`.
__all__ es una señal, no un candado. Los usuarios avanzados siempre pueden acceder a lo que quieran; la convención _nombre comunica intención mejor que intentar forzar privacidad donde Python no la ofrece.
N° 67