1.12 · Módulos y paquetes¶
Objetivos. Al terminar este capítulo podrás: reutilizar código con
import; separar un programa en módulos con responsabilidades claras; usar el
bloque if __name__ == "__main__"; y organizar varios módulos como un paquete
con importaciones relativas.
Evidencia de logro. Convertirás parte del asistente de notas en un paquete pequeño: la lógica quedará en un módulo reutilizable y la interfaz de terminal en otro módulo que solo se ejecutará cuando se lance como programa.
Contexto y motivación¶
Un fichero Python puede crecer hasta ser difícil de leer y probar. Un módulo
es un fichero .py que reúne código relacionado; un paquete es una carpeta
que agrupa módulos que forman una misma unidad.
Separar responsabilidades permite responder preguntas concretas: ¿dónde se carga una nota?, ¿qué código depende de la terminal?, ¿qué puedo reutilizar en una API? Un módulo bien delimitado reduce el acoplamiento y hace visible cada responsabilidad.
Vocabulario¶
| Término | Significado |
|---|---|
| Módulo | Fichero .py importable |
| Paquete | Carpeta que agrupa módulos relacionados |
| Importar | Hacer disponible código de otro módulo |
| API pública | Nombres que un módulo ofrece para ser usados |
__name__ |
Nombre con el que Python ha cargado un módulo |
__main__ |
Nombre del fichero que se está ejecutando como programa |
| Importación relativa | Importación desde el paquete actual (.modulo) |
Prerrequisitos¶
1.8 · Funciones, 1.10 · Archivos y 1.11 · Excepciones y logging.
1. Un módulo es un fichero reutilizable¶
Supón que guardas este código en conversiones.py:
"""Conversiones de temperatura."""
def celsius_a_fahrenheit(celsius):
return celsius * 9 / 5 + 32
ESCALA_POR_DEFECTO = "C"
Otro fichero puede importarlo:
import conversiones
resultado = conversiones.celsius_a_fahrenheit(20)
print(resultado)
print(conversiones.ESCALA_POR_DEFECTO)
Salida esperada:
import conversiones carga el módulo una vez y obliga a escribir el prefijo
conversiones.. Ese prefijo hace explícito de dónde viene cada nombre.
También existen estas formas:
from conversiones import celsius_a_fahrenheit
from conversiones import celsius_a_fahrenheit as a_fahrenheit
print(celsius_a_fahrenheit(0))
print(a_fahrenheit(10))
Salida esperada:
Usa from modulo import nombre cuando el nombre sea claro y no haya riesgo de
colisión. Evita from modulo import *: hace difícil saber de dónde sale cada
variable y puede sobrescribir nombres sin avisar.
2. Separar la ejecución de la importación¶
Cuando Python importa un módulo, ejecuta sus instrucciones de nivel superior. Por eso un módulo reutilizable no debería arrancar una aplicación al importarse.
# saludo.py
def saludar(nombre):
return f"Hola, {nombre}"
def main():
print(saludar("Ana"))
if __name__ == "__main__":
main()
Si ejecutas uv run python saludo.py, __name__ vale "__main__" y se imprime
el saludo. Si otro módulo hace import saludo, __name__ vale "saludo" y
solo quedan disponibles las funciones: no se imprime nada automáticamente.
Haz la prueba
Crea saludo.py y probar_saludo.py con import saludo y
print(saludo.saludar("Luis")). Si al importar aparece el saludo de
main(), la condición __name__ está ausente o mal escrita.
3. De un script a un paquete¶
Una primera separación del asistente de notas puede tener esta forma:
notas/almacenamiento.py contiene la lógica de datos:
# notas/almacenamiento.py
import json
from pathlib import Path
def cargar(ruta):
try:
with Path(ruta).open(encoding="utf-8") as fichero:
return json.load(fichero)
except FileNotFoundError:
return []
def guardar(notas, ruta):
destino = Path(ruta)
destino.parent.mkdir(parents=True, exist_ok=True)
with destino.open("w", encoding="utf-8") as fichero:
json.dump(notas, fichero, ensure_ascii=False, indent=2)
main.py contiene la orquestación de la aplicación:
# main.py
from notas.almacenamiento import cargar, guardar
def main():
ruta = "practica/notas_modulos.json"
notas = cargar(ruta)
notas.append("Separar responsabilidades")
guardar(notas, ruta)
print(f"Notas guardadas: {len(notas)}")
if __name__ == "__main__":
main()
Al ejecutar desde la carpeta que contiene main.py:
Salida esperada:
Es la primera vez que lo ejecutas, así que aún no hay nada escrito. A partir de la segunda cargará lo que ya hay en el fichero, y el número irá creciendo.
main.py conoce las funciones cargar y guardar, pero no necesita saber cómo
se abre el JSON. En una API futura podremos reutilizar el almacenamiento sin
copiar la lógica.
3.1 __init__.py y la API del paquete¶
__init__.py se ejecuta al importar el paquete y puede exponer una API pequeña:
Ahora también es posible escribir:
Salida esperada:
El punto de .almacenamiento significa «desde este mismo paquete». Fíjate en que
el bloque anterior y este importan lo mismo de dos maneras distintas: la primera
va al submódulo y luego toma un nombre; esta segunda usa lo que __init__.py
expone. Para quien usa tu paquete da igual: por eso conviene que __init__.py
reúna lo que quieres ofrecer.
__all__ documenta los nombres que consideramos públicos cuando alguien usa una
importación con *; no sustituye a una documentación clara ni protege contra
accesos directos a otros nombres.
¿Es obligatorio __init__.py?
Python moderno admite paquetes de espacio de nombres sin ese fichero, pero
un __init__.py explícito hace visible la intención, permite definir una
API y resulta más sencillo para un curso y para herramientas de desarrollo.
En los paquetes del curso lo usaremos de forma explícita.
4. Importaciones absolutas y relativas¶
Dentro de un paquete puedes importar de dos maneras. Son dos alternativas, no dos líneas para escribir a la vez: escribes una o la otra y el resultado es el mismo.
# Desde cualquier punto del proyecto, empezando por el nombre del paquete:
from notas.almacenamiento import cargar
# Desde dentro del paquete notas, empezando por un punto:
from .almacenamiento import cargar
Usa la absoluta si el código está fuera del paquete; la relativa es más cómoda
dentro de él, porque no repites el nombre del paquete y sigue funcionando si
algún día lo renombras. No las pongas las dos: la segunda sobrescribe el
nombre cargar de la primera.
La importación relativa solo funciona cuando el módulo forma parte de un
paquete. Si ejecutas notas/almacenamiento.py directamente y aparece
ImportError: attempted relative import, ejecuta el punto de entrada desde la
raíz del proyecto (uv run python main.py) o usa uv run python -m
notas.modulo (ejecutar como módulo, sin .py) cuando el módulo esté diseñado
para ello.
5. Qué ocurre durante una importación¶
Python busca módulos en rutas conocidas, entre ellas el directorio del programa, el entorno virtual y las bibliotecas instaladas:
Salida esperada:
La segunda línea siempre es True: tu entorno virtual tiene sus paquetes en una
carpeta site-packages, y eso es lo que comprueba. La primera no se puede
fijar, y por eso el bloque es de solo sintaxis: si ejecutas un script,
sys.path[0] es la carpeta de ese script; si lo pruebas en el REPL o en un
cuaderno, es una cadena vacía.
Esa variabilidad no es un defecto: es la evidencia de que Python busca módulos en rutas que dependen del punto de entrada, no de una lista fija. Por eso una importación puede funcionar desde un script y fallar desde otro sitio.
No añadas carpetas a sys.path para arreglar una estructura desordenada. Es una
señal de que conviene ejecutar desde la raíz del proyecto o empaquetar mejor el
código. sys.path se inspecciona para diagnosticar, no como solución habitual.
Importaciones condicionales
A veces una función opcional necesita una dependencia pesada. Se puede importar dentro de esa función y lanzar un mensaje claro si falta, pero no uses importaciones condicionales para esconder una dependencia necesaria del proyecto. Las dependencias y su instalación se estudian en 1.15.
6. Documentar un módulo¶
Un docstring de módulo explica su propósito y una función documenta su contrato:
"""Operaciones de almacenamiento de notas en JSON."""
def guardar(notas, ruta):
"""Guarda una lista de notas en la ruta indicada."""
...
La documentación debe decir qué entra, qué sale y qué errores previsibles puede
producir. Las pruebas con assert y pytest se tratarán de manera sistemática
en el capítulo 1.17; aquí céntrate en que el límite entre módulos sea claro.
Aplicación práctica: refactorizar el asistente¶
Compara estas responsabilidades:
| Módulo | Responsabilidad | No debería hacer |
|---|---|---|
almacenamiento.py |
Leer y guardar datos | Pedir input() |
validacion.py |
Comprobar valores | Escribir JSON |
main.py |
Coordinar la interacción | Reimplementar cargar() |
Una separación útil no consiste en crear muchos ficheros, sino en evitar que un
cambio de interfaz obligue a tocar la lógica de datos. Como comprobación,
cambia en main.py la ruta JSON por otra y verifica que almacenamiento.py no
necesita cambios.
Errores frecuentes¶
- Ejecutar un módulo interno desde la carpeta equivocada y obtener
ModuleNotFoundError. - Olvidar
if __name__ == "__main__":y ejecutar efectos secundarios al importar. - Usar
from modulo import *y perder el origen de los nombres. - Crear importaciones circulares:
aimportabybimportaa. - Importar desde el paquete el módulo equivocado por tener dos ficheros con el mismo nombre.
- Cambiar
sys.pathen cada script en vez de corregir la estructura o el punto de ejecución. - Hacer que un módulo de almacenamiento dependa de la terminal o de un
input.
Práctica de transferencia¶
- Separa el contador de palabras de 1.7 en
texto.pyymain.py. - Añade una función
resumir(notas)al módulo de almacenamiento sin imprimir dentro de ella. - Ejecuta
main.pyy después importaresumirdesde otro script; comprueba que importar no imprime mensajes inesperados. - Crea un paquete
conversionescon dos módulos y un__init__.pyque exponga solo dos funciones. - Dibuja la dependencia entre los módulos y señala una importación que sería circular si se añadiera sin criterio.
Producto evaluable¶
Amplía el asistente en un proyecto con esta estructura mínima:
Entrega un script o cuaderno 09_modulos.py que:
- Importe funciones desde el paquete, sin duplicar su implementación.
- Ejecute una operación normal y una entrada inválida.
- Muestre que
main.pyes punto de entrada y que importar los módulos no arranca la aplicación. - Incluya un diagrama o una tabla de dependencias y justifique una decisión de separación.
Criterio de aceptación: cada módulo tiene una responsabilidad reconocible,
las importaciones funcionan desde la raíz del proyecto y no se modifica
sys.path para ocultar un problema de estructura.
Formato de entrega: proyecto ejecutable, cuaderno marimo o scripts acompañados de una salida reproducible, interpretación y conclusión.
Resumen y referencia rápida¶
| Necesitas | Patrón |
|---|---|
| Importar un módulo | import modulo |
| Importar un nombre | from modulo import funcion |
| Punto de entrada | if __name__ == "__main__": |
| Paquete actual | from .modulo import nombre |
| API pública | __all__ = ["nombre"] |
| Diagnosticar búsqueda | sys.path |
Siguiente: 1.13 · Programación orientada a objetos.