Módulo 6: Estructurar un paquete real

6. La API pública con `__all__` y re-exportar en `__init__.py`

Descripción de la cápsula

Tu paquete reservo tiene submódulos con muchas piezas: model.py define Room, Member, Booking, Calendar, BookingService; pricing.py define price_cents, refund_cents, y la constante interna TIER_DISCOUNTS. Cuando alguien importa reservo, ¿qué de todo eso debería poder usar? ¿Todo? ¿Solo algunas? Esta cápsula responde esa pregunta: cómo definir la API pública del paquete — el contrato de lo que expones hacia afuera — con dos herramientas, re-exportar en __init__.py y __all__.

Re-exportar es subir nombres desde los submódulos al nivel del paquete, para que el usuario escriba from reservo import BookingService en vez de tener que conocer que BookingService vive en reservo.model. __all__ es una lista en __init__.py que hace dos cosas: controla qué trae from reservo import *, y documenta cuál es la API pública (lo que es contrato estable) frente a lo que es detalle interno que puede cambiar. Vas a ver las dos en acción, con evidencia ejecutada: un __init__.py vacío que hace fallar from reservo import BookingService, el re-export que lo arregla, y __all__ conteniendo la fuga de nombres internos en import *.

Conexión con el módulo

La cápsula 05 estableció que la librería es lo que otros consumen (la CLI y cualquier segundo frontend). Esta decide qué parte de esa librería es pública. El __init__.py que re-exporta usa los imports relativos de la cápsula 03 (from .model import BookingService). Y prepara la cápsula 07: __version__ también vive en __init__.py, junto a la API pública, como parte de lo que el paquete expone. Definir la API pública es la penúltima pieza de la estructura de reservo.


Analogía: el menú del restaurante

Un restaurante tiene una cocina llena de ingredientes, técnicas y preparaciones (los submódulos y todas sus piezas). Pero al cliente no le entregan la cocina entera — le entregan un menú: la lista curada de lo que puede pedir. El menú es la API pública. Detrás hay cien cosas más (la mise en place, las salsas base, los cortes intermedios), pero esas son internas: el cocinero las usa, el cliente no las pide.

El menú cumple dos funciones. Primero, facilita: el cliente lee "risotto de hongos" sin tener que saber en qué olla se hace ni qué caldo lleva. Segundo, es un contrato: lo que está en el menú, el restaurante se compromete a servirlo consistentemente; lo que está solo en la cocina puede cambiar cuando el chef quiera, sin avisar. __all__ y las re-exportaciones son ese menú: dicen "esto es lo que ofrecemos y mantenemos", y esconden el resto de la cocina.


El problema: sin re-exportar, el usuario tiene que conocer la estructura interna

Por defecto, un __init__.py vacío no expone nada del paquete al nivel de reservo. Miremos qué pasa con un paquete cuyo __init__.py está vacío pero cuyo model.py define BookingService:

python -c "from reservo_empty import BookingService"

Qué esperar: falla. BookingService existe, pero en reservo_empty.model, no en reservo_empty a secas. El __init__.py vacío no lo subió al nivel del paquete.

ImportError: cannot import name 'BookingService' from 'reservo_empty'

El usuario puede importarlo, pero tiene que conocer la estructura interna y nombrar el submódulo:

python -c "from reservo_empty.model import BookingService; print('OK:', BookingService)"
OK: <class 'reservo_empty.model.BookingService'>

Esto funciona, pero es frágil: obliga al usuario a saber que BookingService vive en model.py. Si mañana mueves esa clase a otro submódulo (services.py), rompes el código de todos los que escribieron from reservo.model import BookingService. La estructura interna del paquete quedó expuesta como parte del contrato — y eso es exactamente lo que no queremos.


La solución: re-exportar en __init__.py

Re-exportar es importar los nombres públicos en el __init__.py, subiéndolos al nivel del paquete. El __init__.py de reservo lo hace con imports relativos:

# src/reservo/__init__.py
"""reservo: reserva de salas de coworking, empaquetada como librería y CLI."""

from .model import Room, Member, Booking, Calendar, BookingService
from .pricing import price_cents, refund_cents

Ahora BookingService está disponible directamente en reservo:

python -c "from reservo import BookingService; print(BookingService)"

Qué esperar: funciona sin nombrar model. El usuario importa desde el paquete, no desde el submódulo interno.

<class 'reservo.model.BookingService'>

Fíjate en lo que ganamos:

  • La API es estable frente a reorganizaciones internas. El usuario escribe from reservo import BookingService. Si mañana mueves BookingService de model.py a services.py, solo cambias una línea en __init__.py (from .services import BookingService) y el código de todos sigue funcionando. La estructura interna quedó oculta tras el __init__.py.
  • La importación es más corta y clara. from reservo import BookingService, Room, price_cents en vez de tres imports desde tres submódulos distintos.
  • El __init__.py es el menú. Leerlo te dice, de un vistazo, qué ofrece el paquete: Room, Member, Booking, Calendar, BookingService, price_cents, refund_cents. La API pública en un solo lugar.

__all__: qué controla y qué documenta

Re-exportar sube los nombres, pero hay un detalle: cuando alguien hace from reservo import *, Python trae todos los nombres públicos del namespace del __init__.py — incluidos los que importaste como detalle interno. Veámoslo con un __init__.py que re-exporta BookingService pero también hace import argparse (un detalle interno):

# __init__.py SIN __all__
from .model import BookingService
import argparse   # detalle interno, NO debería ser API pública
python -c "from reservo_leak import *; print(sorted(n for n in dir() if not n.startswith('_')))"

Qué esperar: import * fuga nombres que no son API pública — argparse (una librería que importaste) y model (el submódulo). El usuario los recibe como si fueran parte de tu paquete.

['BookingService', 'argparse', 'model']

Eso es una fuga: argparse y model no son parte de lo que ofreces, pero import * los trajo. La solución es __all__: una lista que declara explícitamente qué nombres son la API pública. Con __all__, import * trae solo lo listado:

# __init__.py CON __all__
from .model import BookingService
import argparse
__all__ = ["BookingService"]
python -c "from reservo_leak import *; print(sorted(n for n in dir() if not n.startswith('_')))"

Qué esperar: ahora import * trae solo BookingService. argparse y model quedaron fuera — son internos.

['BookingService']

Las dos funciones de __all__

__all__ hace dos cosas, y la segunda es la más importante:

  1. Controla from paquete import *. Solo los nombres en __all__ se importan con el asterisco. Sin __all__, import * trae todo lo que no empiece con _, incluyendo fugas.

  2. Documenta la API pública. Aunque casi nunca uses import * en código serio (y con razón), __all__ es la declaración explícita de qué es contrato. Un lector —o una herramienta— mira __all__ y sabe: "esto es lo que el paquete promete mantener; lo demás es interno y puede cambiar". Es documentación ejecutable del menú.

El __all__ completo de reservo:

# src/reservo/__init__.py
from .model import Room, Member, Booking, Calendar, BookingService
from .pricing import price_cents, refund_cents

__all__ = [
    "Room",
    "Member",
    "Booking",
    "Calendar",
    "BookingService",
    "price_cents",
    "refund_cents",
]

Verifícalo:

python -c "import reservo; print(reservo.__all__)"

Qué esperar: la lista de la API pública, tal como la declaraste.

['Room', 'Member', 'Booking', 'Calendar', 'BookingService', 'price_cents', 'refund_cents']

Nota qué no está en __all__: TIER_DISCOUNTS (la constante interna de pricing), ni argparse, ni los nombres de los submódulos. Esas son piezas de la cocina, no del menú. Siguen accesibles si alguien insiste (reservo.pricing.TIER_DISCOUNTS), pero no son parte del contrato: puedes cambiarlas sin romper a nadie que respete la API pública.


Qué exponer y qué no: el criterio

La regla para decidir qué va en la API pública:

  • Va en la API pública lo que quieres que otros usen y te comprometes a mantener estable: las clases del dominio (Room, BookingService), las funciones de cálculo (price_cents, refund_cents). Son el "menú".
  • NO va lo que es detalle de implementación: constantes internas (TIER_DISCOUNTS), helpers privados, los módulos que importaste (argparse), la estructura de submódulos. Puede cambiar sin aviso.

Una convención de apoyo: los nombres que empiezan con guion bajo (_service, _rooms) son privados por convención — le dicen al lector "esto es interno, no lo uses desde afuera". import * ya los ignora por defecto (no trae nombres con _ inicial), y por lo general no van en __all__. Entre el guion bajo (privado) y __all__ (público explícito), el paquete comunica con claridad qué es contrato y qué es cocina.

Sobre import * en general: aunque __all__ lo controla, from reservo import * rara vez es buena idea en código real — oscurece de dónde viene cada nombre. Se prefiere importar explícito (from reservo import BookingService). El valor de __all__ no es tanto habilitar import * como documentar la API pública. Esa es su razón de ser principal.


Errores comunes

Error 1: __init__.py vacío y exponer la estructura interna

Con un __init__.py vacío, los usuarios escriben from reservo.model import BookingService — clavando el nombre del submódulo en su código. El día que reorganices los submódulos, rompes a todos. Re-exporta en __init__.py para que el usuario importe desde reservo y la estructura interna quede oculta y libre de cambiar.

Error 2: No poner __all__ y dejar fugar detalles internos

Sin __all__, from reservo import * arrastra todo lo público del namespace del __init__.py, incluidos argparse, los submódulos y cualquier helper que importaste. Declara __all__ con solo los nombres que son API pública, para que import * (y la documentación) reflejen el contrato real, no las fugas.

Error 3: Poner en __all__ un nombre que no existe en el namespace

__all__ = ["BookingService", "PriceCalculator"]   # PriceCalculator no existe

Si listas en __all__ un nombre que no importaste ni definiste en el __init__.py, from reservo import * fallará con AttributeError al intentar traerlo. __all__ debe contener solo nombres que estén de verdad en el namespace del paquete (re-exportados o definidos ahí).

Error 4: Confundir "accesible" con "público"

Que algo sea accesible (reservo.pricing.TIER_DISCOUNTS funciona) no lo hace parte de la API pública. Python no impide el acceso a los internos — la privacidad es por convención (el guion bajo, la ausencia de __all__). Un usuario que importa TIER_DISCOUNTS lo hace bajo su propio riesgo: puede desaparecer en la próxima versión. Lo público es lo que está en __all__ y re-exportado; lo demás es cocina.


Ejercicios

Ejercicio 1: Escribir el __init__.py con API pública (Fácil)

Tu paquete reservo tiene model.py (con Room y BookingService) y pricing.py (con price_cents y la constante interna TIER_DISCOUNTS). Escribe el __init__.py que re-exporta la API pública —Room, BookingService, price_cents— y la declara en __all__. TIER_DISCOUNTS NO debe ser pública.

Ver solución
# src/reservo/__init__.py
from .model import Room, BookingService
from .pricing import price_cents

__all__ = ["Room", "BookingService", "price_cents"]

Explicación: re-exportamos con imports relativos los tres nombres que son API pública, subiéndolos al nivel de reservo (para que from reservo import BookingService funcione). __all__ los declara como el contrato. TIER_DISCOUNTS no se re-exporta ni entra en __all__: es un detalle interno de pricing, sigue accesible como reservo.pricing.TIER_DISCOUNTS para quien insista, pero no es parte del menú.

Ejercicio 2: Predecir el resultado de import * (Medio)

Dado este __init__.py, ¿qué imprime python -c "from reservo import *; print(sorted(n for n in dir() if not n.startswith('_')))"? ¿Y si borras la línea de __all__?

# src/reservo/__init__.py
from .model import Room, BookingService
from .pricing import price_cents
import argparse

__all__ = ["Room", "BookingService", "price_cents"]
Ver solución

Con __all__: import * trae solo lo declarado:

['BookingService', 'Room', 'price_cents']

Sin la línea de __all__: import * trae todos los nombres públicos del namespace del __init__.py — incluidos los que se fugan:

['BookingService', 'Room', 'argparse', 'model', 'price_cents', 'pricing']

Explicación: con __all__, import * se limita a los tres nombres del contrato. Sin __all__, fuga argparse (la librería que importaste), y model/pricing (los submódulos que quedaron en el namespace al re-exportar de ellos). Esos tres no son API pública, pero sin __all__ el asterisco los arrastra. Por eso __all__ es el filtro que mantiene el menú limpio.

Ejercicio 3: Sobrevivir a una reorganización interna (Medio)

El usuario escribe from reservo import BookingService. Tú decides mover la clase BookingService de model.py a un nuevo submódulo services.py. ¿Qué archivo(s) tienes que cambiar para que el código del usuario siga funcionando sin que él toque nada? Muestra el cambio.

Ver solución

Solo cambias una línea en src/reservo/__init__.py: la re-exportación de BookingService pasa a apuntar al nuevo submódulo.

# ANTES
from .model import Room, Member, Booking, Calendar, BookingService

# DESPUÉS (BookingService se movió a services.py)
from .model import Room, Member, Booking, Calendar
from .services import BookingService

El __all__ no cambia (BookingService sigue siendo API pública). El código del usuario (from reservo import BookingService) tampoco cambia: sigue importando desde reservo, sin saber ni importarle que la clase se mudó de archivo.

Explicación: este es exactamente el valor de re-exportar en __init__.py. La API pública (from reservo import BookingService) queda desacoplada de la estructura interna. Reorganizas los submódulos como quieras; mientras el __init__.py siga re-exportando los mismos nombres, nadie afuera se entera. Si el usuario hubiera importado from reservo.model import BookingService, tu reorganización habría roto su código — por eso la API pública se consume desde el paquete, no desde los submódulos.


Resumen

En esta cápsula aprendiste:

  • La API pública de un paquete es el contrato de lo que expones y te comprometes a mantener — el "menú" frente a la "cocina" (los detalles internos).

  • Re-exportar en __init__.py (from .model import BookingService) sube los nombres públicos al nivel del paquete, para que el usuario escriba from reservo import BookingService sin conocer la estructura interna. Con __init__.py vacío, esa importación falla con ImportError.

  • Re-exportar desacopla la API de la estructura: mueves una clase de submódulo y solo cambias una línea en __init__.py; el código de los usuarios sigue igual.

  • __all__ hace dos cosas: controla qué trae from reservo import * (evitando fugas como argparse o los submódulos), y documenta la API pública. Su valor principal es documentar el contrato, más que habilitar import *.

  • El criterio: va en la API pública lo que quieres que otros usen y mantendrás estable (clases del dominio, funciones de cálculo); no va lo interno (constantes como TIER_DISCOUNTS, helpers con _, submódulos). Accesible no es lo mismo que público.

  • Verificado ejecutando: from reservo import BookingService funciona; reservo.__all__ lista los 7 nombres públicos; import * con __all__ trae solo esos, sin fugas.

Próxima cápsula: 07-version-and-the-single-source-of-truth.md — La API pública ya está definida en __init__.py. Junto a ella vive __version__ — y ahí surge un problema clásico: ¿dónde vive la versión para que no se desincronice? Vas a ver la fuente única de verdad con importlib.metadata.


Recursos Adicionales

  1. Importing * From a Package (Tutorial oficial) — La explicación canónica de __all__ y cómo controla from package import *. Fuente principal de esta cápsula.

  2. The import system — Submodules and __init__ (docs oficiales) — La referencia de cómo el __init__.py compone el namespace del paquete y cómo se re-exportan submódulos.

  3. PEP 8 — Public and internal interfaces — La recomendación oficial sobre qué es API pública, el rol de __all__ y la convención del guion bajo para lo interno.

  4. Packaging namespace and public API (packaging.python.org) — Guía de la PyPA sobre cómo organizar lo que un paquete expone.