Paquetes de Python y el Sistema de Importación
Aprende cómo funcionan los paquetes de Python: crea un paquete con __init__.py, usa importaciones absolutas y relativas, expón una API pública limpia y evita errores comunes.
Un paquete es un directorio de módulos de Python que se trata como una única unidad importable. Donde un módulo es un solo archivo .py, un paquete es una carpeta — que puede contener muchos módulos y sub-paquetes — que el sistema de importación de Python puede recorrer como un árbol. Este capítulo explica cómo crear paquetes, controlar lo que exponen, escribir importaciones absolutas y relativas correctamente, y evitar los problemas que confunden a los principiantes.
Módulos vs. Paquetes — la Diferencia Clave
Un módulo es un solo archivo .py:
greetings.py ← moduleUn paquete es un directorio que contiene al menos un archivo especial llamado __init__.py:
greetings/ ← package
__init__.py
english.py
spanish.pyAmbos se importan con la misma palabra clave import, pero un paquete te ofrece una jerarquía de espacios de nombres: greetings.english y greetings.spanish son módulos separados, pero comparten el espacio de nombres greetings.
Cuándo usar un módulo vs. un paquete:
| Situación | Usar |
|---|---|
| Una utilidad pequeña y autocontenida | Módulo (un archivo .py) |
| Varios módulos relacionados que quieres bajo un mismo nombre | Paquete (un directorio) |
| Una biblioteca que pretendes distribuir en PyPI | Paquete (con estructura src/) |
El Archivo __init__.py
__init__.py es lo que convierte un directorio en un paquete. Python lo ejecuta la primera vez que se importa el paquete (o cualquiera de sus módulos). Puede estar vacío, o puede:
- Importar nombres de sub-módulos para que estén disponibles a nivel del paquete
- Ejecutar inicialización a nivel del paquete (configuración de logging, comprobaciones de versión, etc.)
- Definir
__all__para controlarfrom package import *
Estructura mínima de un paquete
myapp/
__init__.py ← can be empty
utils.py
config.py# myapp/__init__.py (empty — that is fine)# myapp/utils.py
def greet(name):
return f"Hello, {name}!"Importar desde fuera del paquete:
from myapp.utils import greet
print(greet("Alice")) # Hello, Alice!Exponer nombres a nivel del paquete
Un patrón habitual es importar los nombres más utilizados en __init__.py para que los usuarios puedan escribir from myapp import greet en lugar de from myapp.utils import greet.
# myapp/__init__.py
from .utils import greet
from .config import MAX_RETRIESAhora ambos nombres están disponibles directamente en el paquete:
import myapp
print(myapp.greet("Bob")) # Hello, Bob!
print(myapp.MAX_RETRIES) # whatever config.py definesImportaciones Absolutas
Una importación absoluta siempre parte del paquete de nivel superior o de un directorio en sys.path. Nunca depende de dónde se encuentre el archivo que realiza la importación.
project/
myapp/
__init__.py
utils.py
services/
__init__.py
email.pyDentro de email.py, una importación absoluta tiene este aspecto:
# myapp/services/email.py
from myapp.utils import greet # absolute — starts from the top-level package
def send_welcome(user):
message = greet(user)
print(f"Sending: {message}")Las importaciones absolutas son el estilo por defecto y el recomendado (PEP 8). Son inequívocas independientemente de cómo se invoque Python.
Importaciones Relativas
Una importación relativa usa puntos (.) para navegar por el árbol de paquetes relativo a la ubicación del archivo actual.
.significa el paquete actual..significa el paquete padre...significa el paquete abuelo, y así sucesivamente
# myapp/services/email.py
# One dot — import from myapp.services (same directory)
from . import sms
# Two dots — import from myapp (parent directory)
from ..utils import greetCuándo usar importaciones relativas
Las importaciones relativas son útiles dentro de un paquete cuando quieres dejar claro que greet proviene de este paquete y no de alguna biblioteca externa con el mismo nombre. También facilitan la refactorización porque las importaciones se mueven con el paquete si renombras el directorio de nivel superior.
Advertencia: las importaciones relativas solo funcionan dentro de un paquete. Si ejecutas python myapp/utils.py directamente, Python lo trata como un script independiente, no como parte de un paquete, y una importación relativa lanza ImportError: attempted relative import with no known parent package. Ejecuta el paquete con python -m myapp.utils en su lugar.
# Wrong — runs utils.py as a script, breaking relative imports
$ python myapp/utils.py
# Right — runs utils.py as part of the myapp package
$ python -m myapp.utilsControlar la API Pública con __all__
__all__ es una lista de nombres que from package import * exporta. También documenta qué considera el paquete como público.
# myapp/__init__.py
from .utils import greet, farewell
from .config import MAX_RETRIES
__all__ = ["greet", "MAX_RETRIES"] # farewell is intentionally not exportedAhora from myapp import * trae solo greet y MAX_RETRIES. La función farewell sigue existiendo; simplemente no forma parte de la interfaz pública anunciada. Los nombres con un guion bajo inicial (_private) también se excluyen de import * por convención.
Paquetes Anidados (Sub-paquetes)
Los paquetes pueden contener otros paquetes. Cada subdirectorio necesita su propio __init__.py.
analytics/
__init__.py
reports/
__init__.py
daily.py
weekly.py
charts/
__init__.py
bar.py
pie.pyImporta un módulo profundamente anidado con la ruta completa con puntos:
from analytics.reports.daily import generate_report
from analytics.charts.bar import BarChartO bien, si analytics/__init__.py los expone:
# analytics/__init__.py
from .reports.daily import generate_report# caller
from analytics import generate_report¿Hasta qué profundidad deberías llegar?
Un paquete con tres o cuatro niveles de profundidad suele ser una señal de que ha crecido demasiado y debería dividirse en paquetes de nivel superior separados (instalables de forma independiente). Para la mayoría de los proyectos, dos niveles (package.module) es suficiente.
Un Ejemplo Práctico: Construir un Paquete geometry
Construyamos un paquete pequeño pero realista paso a paso.
Estructura de directorios
geometry/
__init__.py
shapes.py
conversions.pyshapes.py
# geometry/shapes.py
import math
def circle_area(radius):
"""Return the area of a circle with the given radius."""
if radius < 0:
raise ValueError("radius must be non-negative")
return math.pi * radius ** 2
def rectangle_area(width, height):
"""Return the area of a rectangle."""
return width * height
def triangle_area(base, height):
"""Return the area of a triangle."""
return 0.5 * base * heightconversions.py
# geometry/conversions.py
def degrees_to_radians(degrees):
"""Convert degrees to radians."""
import math
return degrees * math.pi / 180
def radians_to_degrees(radians):
"""Convert radians to degrees."""
import math
return radians * 180 / math.pi__init__.py — exponer los nombres clave
# geometry/__init__.py
"""
geometry — simple 2-D geometry utilities.
Public API:
circle_area(radius) -> float
rectangle_area(width, height) -> float
triangle_area(base, height) -> float
degrees_to_radians(degrees) -> float
radians_to_degrees(radians) -> float
"""
from .shapes import circle_area, rectangle_area, triangle_area
from .conversions import degrees_to_radians, radians_to_degrees
__all__ = [
"circle_area",
"rectangle_area",
"triangle_area",
"degrees_to_radians",
"radians_to_degrees",
]Usar el paquete
# main.py (sits next to the geometry/ directory)
import geometry
print(geometry.circle_area(5)) # 78.53981633974483
print(geometry.rectangle_area(4, 6)) # 24
print(geometry.degrees_to_radians(90)) # 1.5707963267948966O con importaciones selectivas:
from geometry import circle_area, degrees_to_radians
print(circle_area(3)) # 28.274333882308138
print(degrees_to_radians(180)) # 3.141592653589793Paquetes de Espacio de Nombres (Python 3.3+)
Desde Python 3.3, un directorio sin __init__.py es un paquete de espacio de nombres. Python fusiona todos los directorios con el mismo nombre en sys.path en un único paquete lógico. Esto es principalmente útil para grandes organizaciones que dividen un único paquete entre múltiples repositorios o directorios de instalación.
Para el desarrollo cotidiano, incluye siempre __init__.py. Hace que tu intención sea inequívoca y funciona en todas las versiones de Python.
Cómo Encuentra Python los Paquetes
Cuando escribes import geometry, Python busca en sys.path en orden:
- El directorio del script en ejecución (o el directorio actual en modo interactivo)
- Los directorios en la variable de entorno
PYTHONPATH - Los directorios de la biblioteca estándar
- El directorio
site-packages(donde viven los paquetes instalados con pip)
import sys
print(sys.path)El directorio del paquete debe estar directamente dentro de una de estas ubicaciones. Si geometry/ está en /home/alice/projects/, Python no lo encontrará a menos que /home/alice/projects/ esté en sys.path.
Consejo: usa un entorno virtual e instala tu paquete en modo de desarrollo (pip install -e .) para que Python siempre lo encuentre sin manipulación manual de sys.path.
Distribuir un Paquete
Para compartir un paquete con otros (o instalarlo con pip), necesitas un pyproject.toml en la raíz del proyecto:
my_project/
pyproject.toml ← build metadata
src/
geometry/
__init__.py
shapes.py
conversions.pyUn pyproject.toml mínimo:
[build-system]
requires = ["setuptools>=68", "wheel"]
build-backend = "setuptools.backends.legacy:build"
[project]
name = "geometry"
version = "0.1.0"
description = "Simple 2-D geometry utilities"
requires-python = ">=3.9"Instálalo localmente en modo editable durante el desarrollo:
pip install -e .Ahora import geometry funciona en cualquier lugar de tu entorno virtual, independientemente de tu directorio actual.
Errores Comunes
Falta el archivo __init__.py
Si olvidas __init__.py, Python 3 trata el directorio como un paquete de espacio de nombres (que generalmente sigue funcionando), pero Python 2 lo ignora por completo. Sé explícito: añade siempre __init__.py.
Nombrar un paquete igual que un módulo de la biblioteca estándar
Evita nombres como math/, json/, os/, email/. Python puede importar tu paquete en lugar del de la biblioteca estándar, rompiendo código no relacionado.
Ejecutar un módulo de paquete como script
Como se indicó anteriormente, ejecutar python myapp/services/email.py directamente rompe las importaciones relativas. Usa python -m myapp.services.email en su lugar.
Importaciones circulares entre módulos del mismo paquete
Si shapes.py importa de conversions.py y conversions.py importa de shapes.py, tienes una importación circular. Los síntomas incluyen ImportError o nombres que aparecen inesperadamente como None. La solución suele ser mover la lógica compartida a un tercer módulo, o retrasar la importación al interior del cuerpo de una función.
# Delayed import — breaks the cycle at module load time
def some_function():
from .shapes import circle_area # imported only when the function is called
...ImportError al usar importaciones relativas fuera de un paquete
# Will raise: ImportError: attempted relative import with no known parent package
# if run as: python myapp/utils.py
from . import config # relative import inside utils.pyEjecútalo como python -m myapp.utils o reestructura para que el punto de entrada sea un script separado que importe el paquete.
Resumen
| Concepto | Descripción breve |
|---|---|
| Paquete | Un directorio con __init__.py que contiene módulos |
__init__.py | Convierte un directorio en paquete; se ejecuta en la primera importación |
| Importación absoluta | from myapp.utils import greet — siempre desde la raíz |
| Importación relativa | from ..utils import greet — relativa al archivo actual |
__all__ | Lista los nombres exportados por from package import * |
| Paquete de espacio de nombres | Directorio sin __init__.py; solo Python 3.3+ |
| Instalación editable | pip install -e . — paquete encontrado en cualquier lugar del venv |
Consulta Módulos de Python para la organización de código en un solo archivo, Python pip para instalar paquetes de terceros, y Entornos Virtuales de Python para mantener aisladas las dependencias del proyecto.