Módulos, paquetes y entornos virtuales
Separar el código en archivos reutilizables y aislar las dependencias de cada proyecto
Un programa pequeño cabe en un archivo; uno real necesita módulos, paquetes y dependencias con versión fija. Aprenderás cómo Python encuentra lo que importas, cómo se ejecuta un módulo una sola vez y cómo crear un entorno virtual que haga tu proyecto reproducible.
- Importar módulos y nombres concretos, incluidos alias e imports relativos
- Explicar el orden de búsqueda de sys.path y el papel de PYTHONPATH
- Proteger el código de prueba con if __name__ == "__main__"
- Crear un paquete con __init__.py y una estructura de proyecto clara
- Crear un entorno virtual, instalar dependencias y fijarlas en requirements.txt
Antes necesitas: Archivos, JSON y CSV · Funciones
En esta lección
- ¿Qué es un módulo y qué es un paquete?
- ¿Para qué sirve organizar el código así?
- ¿Cómo funciona por dentro? La búsqueda y la caché de imports
- Sintaxis: import, from y alias
- Ejemplo básico: usar la biblioteca estándar
- Ejemplo intermedio: tu primer módulo propio
- Caso práctico: un paquete con dos módulos
- Visualización: qué pasa en un import repetido
- Errores comunes
- Buenas prácticas
- Ejercicios
- Soluciones explicadas
- Resumen
- Evaluación
Hasta ahora tus programas viven en un solo archivo. Eso funciona mientras el código cabe en la pantalla, pero un proyecto real mezcla lectura de datos, limpieza, modelo e informes, y todo en un archivo se vuelve imposible de navegar. Python resuelve el problema con módulos: archivos .py que se importan entre sí.
Hay una segunda pieza que casi nadie enseña al principio: el entorno virtual. Sin él, cada proyecto comparte las mismas bibliotecas instaladas en el sistema, y actualizar una rompe la otra. Juntas, ambas ideas son la diferencia entre un script de práctica y un proyecto que otra persona puede ejecutar dentro de seis meses.
¿Qué es un módulo y qué es un paquete?
Un módulo es un archivo .py cuyo contenido puedes cargar desde otro archivo. Su nombre de módulo es el nombre del archivo sin la extensión: conversiones.py se importa como conversiones. Un paquete es una carpeta de módulos con un archivo __init__.py que marca dónde empieza el paquete; ventas/ con limpieza.py y resumen.py se importa como ventas.
Piensa en un módulo como una caja de herramientas etiquetada: no vuelcas la caja entera sobre la mesa, sacas la herramienta que necesitas. Un paquete es un mueble con cajones: agrupa herramientas por tema y te obliga a decir en qué cajón está cada una. El entorno virtual es el taller propio de cada proyecto, con sus propias herramientas y versiones.
¿Para qué sirve organizar el código así?
- Reutilizar: la función que normaliza textos se escribe una vez y la usan el informe, la API y las pruebas.
- Separar responsabilidades: un archivo carga datos, otro los limpia, otro entrena; cada uno se puede leer y probar por separado.
- Evitar choques de nombres:
ventas.limpiarytexto.limpiarpueden convivir porque viven en paquetes distintos. - Fijar dependencias:
requirements.txtdice exactamente qué versión de NumPy o pytest necesita el proyecto. - Colaborar sin sorpresas: quien clona el repositorio crea su entorno virtual, instala y obtiene el mismo resultado.
¿Cómo funciona por dentro? La búsqueda y la caché de imports
Cuando escribes import math, Python no adivina dónde está. Recorre una lista ordenada de rutas, sys.path, y se queda con la primera coincidencia. El orden es: el directorio del script que estás ejecutando (o el directorio actual si usas -c o el REPL), las rutas de la variable de entorno PYTHONPATH, y por último las carpetas de la biblioteca estándar y de los paquetes instalados.
Ese orden explica un error clásico: si tu archivo se llama random.py, Python lo encuentra antes que al random de la biblioteca estándar y tu propio archivo la sustituye. La solución no es pelear con sys.path, es renombrar tu archivo.
Antes de leer el archivo, Python mira sys.modules, un diccionario con todo lo ya importado. Si el módulo está ahí, lo reutiliza y no vuelve a ejecutarlo. Por eso el código de nivel superior corre una sola vez aunque lo importen diez archivos. La primera importación ejecuta el archivo entero, de arriba abajo, y deja un objeto módulo con sus nombres; las siguientes solo enlazan ese objeto.
Cada módulo tiene un atributo __name__. Si lo ejecutas directamente con python3 archivo.py, vale "__main__"; si lo importas desde otro archivo, vale el nombre del módulo ("conversiones"). Esa diferencia permite dejar pruebas al final de un archivo sin que se ejecuten al importarlo, siempre que las encierres en if __name__ == "__main__":.
Sintaxis: import, from y alias
# 1. Importa el módulo completo: usas módulo.nombre
import statistics
# 2. Importa un nombre concreto: lo usas directo
from math import sqrt
# 3. Alias para abreviar o evitar choques
import numpy as np
from statistics import mean as promedio
# 4. Dentro de un paquete, import relativo al paquete actual
# from .limpieza import normalizar_sede
# from ..utilidades import formatear
print(statistics.mean([2, 4, 6]))
print(sqrt(81))
print(promedio([10, 20]))import statistics enlaza el nombre statistics con el módulo; para usar la media necesitas statistics.mean. from statistics import mean trae solo la función, pero pierdes el contexto de dónde venía. Los alias son útiles cuando el nombre es largo (numpy como np) o cuando dos módulos exportan el mismo nombre.
Ejemplo básico: usar la biblioteca estándar
No hace falta instalar nada para empezar: la biblioteca estándar trae matemáticas, estadística, fechas y archivos. Este programa calcula el área de tres círculos y su media.
import math
from statistics import mean
radios = [1.0, 2.0, 3.0]
areas = [math.pi * r ** 2 for r in radios]
print(round(areas[0], 2))
print(round(mean(areas), 2))La primera línea trae el módulo math completo; la segunda trae únicamente mean. La comprensión calcula cada área con math.pi, y mean promedia la lista. La salida es 3.14 y 14.66: la media de 3.14, 12.57 y 28.27. Si solo necesitaras una función, from math import pi sería suficiente, pero entonces el nombre pi quedaría suelto y menos claro.
import math
print(math.sqrt(16))#include <cmath>
#include <iostream>
int main() {
std::cout << std::sqrt(16.0) << std::endl;
return 0;
}En C++ el `#include` copia el contenido de la cabecera durante la compilación y necesitas `std::`. En Python, `import` carga un objeto módulo en tiempo de ejecución y los nombres viven dentro de él.
Ejemplo intermedio: tu primer módulo propio
Crea una carpeta y dentro dos archivos. El primero, conversiones.py, define constantes y funciones. El segundo, main.py, las usa. Ambos deben estar en la misma carpeta para que la búsqueda de imports los encuentre.
"""Conversiones de unidades usadas en el curso."""
FACTOR_LIBRA_KG = 0.45359237
def libras_a_kilos(libras: float) -> float:
return libras * FACTOR_LIBRA_KG
def kilos_a_libras(kilos: float) -> float:
return kilos / FACTOR_LIBRA_KG
if __name__ == "__main__":
print(libras_a_kilos(150))import conversiones
peso_lb = 150
print(round(conversiones.libras_a_kilos(peso_lb), 1))Ejecutar python3 conversiones.py imprime 68.0388555, porque ahí __name__ vale "__main__" y la prueba se ejecuta. Ejecutar python3 main.py imprime 68.0: importar conversiones define las funciones, pero el bloque de prueba queda apagado. Ese es todo el truco del guardián, y es la razón por la que puedes importar sin efectos secundarios.
Caso práctico: un paquete con dos módulos
Cuando un módulo crece, se parte en un paquete. Esta estructura separa la limpieza de textos del cálculo del resumen, y expone una interfaz corta desde __init__.py: quien use el paquete importa ventas y no necesita saber en qué archivo vive cada función.
proyecto/
├── main.py
├── requirements.txt
└── ventas/
├── __init__.py
├── limpieza.py
└── resumen.py"""Herramientas para analizar ventas."""
from .limpieza import normalizar_sede
from .resumen import total_por_sede
__all__ = ["normalizar_sede", "total_por_sede"]from .limpieza import normalizar_sede
def total_por_sede(registros):
totales = {}
for sede, monto in registros:
clave = normalizar_sede(sede)
totales[clave] = totales.get(clave, 0) + monto
return totalesfrom ventas import total_por_sede
registros = [("lima ", 120.0), ("Cusco", 80.5), ("LIMA", 60.0)]
print(total_por_sede(registros))El punto de from .limpieza significa «dentro de este mismo paquete». La salida es {'Lima': 180.0, 'Cusco': 80.5}: lima y LIMA se normalizan a la misma clave y sus montos se suman. __all__ no bloquea imports, pero documenta qué nombres forman la interfaz pública y es lo que usa from ventas import *.
Crea un entorno virtual antes de instalar nada. En macOS o Linux, python3 -m venv .venv crea la carpeta, source .venv/bin/activate lo activa y el prompt muestra (.venv). En Windows la activación es .venv\Scripts\activate. Después, pip install numpy instala dentro del entorno, no en el sistema.
| Comando | Qué hace |
|---|---|
python3 -m venv .venv | Crea un entorno aislado en la carpeta .venv |
source .venv/bin/activate | Activa el entorno en macOS y Linux |
pip install numpy | Instala en el entorno activo, no en todo el sistema |
pip freeze > requirements.txt | Escribe las versiones exactas instaladas |
pip install -r requirements.txt | Reproduce esas versiones en otra máquina |
Visualización: qué pasa en un import repetido
Avanza paso a paso. Observa que el segundo import math no vuelve a ejecutar el módulo y que __name__ vale "__main__" porque el archivo se está ejecutando, no importando.
import sysimport mathimport mathprint(math.sqrt(81))print("math" in sys.modules)print(__name__)sys es un módulo integrado: ya está cargado, así que solo se enlaza el nombre.—
Avanza paso a paso: observa la línea resaltada, las variables y la salida.
Errores comunes
| Mensaje | Por qué ocurre | Cómo se arregla |
|---|---|---|
ModuleNotFoundError: No module named 'requests' | El paquete no está instalado en el entorno activo | Activa el entorno y ejecuta pip install requests |
ImportError: cannot import name 'media' from 'statistics' | El nombre no existe o está mal escrito | Revisa la ortografía; en la biblioteca estándar es mean |
ImportError: attempted relative import with no known parent package | Ejecutaste un módulo del paquete directamente con python3 ventas/resumen.py | Ejecuta python3 -m ventas.resumen desde la raíz del proyecto |
ModuleNotFoundError: No module named 'ventas' | Estás en otra carpeta y la raíz del proyecto no está en sys.path | Ejecuta desde la raíz o añade la ruta con PYTHONPATH=. |
cd ventas
python3 resumen.py
# ImportError: attempted relative import with no known parent packagepython3 -m ventas.resumen
# Python conoce el paquete y resuelve el puntoEl import relativo necesita saber a qué paquete pertenece el archivo. Al ejecutar el archivo suelto, Python lo trata como `__main__` y pierde ese contexto; con `-m` lo carga como `ventas.resumen` y el punto se resuelve.
Buenas prácticas
- No nombres un archivo como un módulo de la biblioteca estándar:
random.py,json.pyotypes.pyte van a traicionar. - Importa arriba del archivo y en grupos: biblioteca estándar, terceros, módulos propios.
- Usa
from x import ycuando el nombre sea inequívoco; si no, prefiereimport xpara conservar el contexto. - Deja en
__init__.pysolo la interfaz pública y los imports cortos; la lógica va en los módulos internos. - Ejecuta los scripts de un paquete con
python3 -m paquete.modulo, no con la ruta del archivo. - Fija versiones en
requirements.txty mantén.venvfuera del control de versiones.
Ejercicios
Un módulo con guardián
temperaturas.py con dos funciones: c_a_f(celsius) y f_a_c(fahrenheit). Añade un bloque if __name__ == "__main__": que imprima la conversión de 25 °C y de 212 °F. Ejecuta el archivo y después impórtalo desde otro archivo: comprueba que al importarlo no se imprime nada.Pista
El guardián compara __name__ con la cadena "__main__". Al importar, __name__ es "temperaturas" y el bloque se salta.
Convertir un script en módulo reutilizable
analisis.py con una función resumen(ventas) que devuelva un diccionario con n, media (redondeada a 2 decimales) y maximo. Deja la impresión dentro del guardián. Después escribe informe.py que haga from analisis import resumen e imprima el resultado de una lista de prueba.Pista
Dentro de resumen usa len, statistics.mean y max. La función devuelve datos; solo el bloque __main__ imprime.
import as st from math import datos = [2, 4, 6] print((datos)) print(sqrt(81))
El alias `st` se usa como prefijo. La función que falta es la raíz cuadrada.
Soluciones explicadas
En el primer ejercicio, la tentación es dejar las impresiones al final del archivo sin guardián. Funciona mientras ejecutas ese archivo, pero al importarlo desde otro se imprime basura en medio del programa. El guardián convierte el archivo en dos cosas a la vez: script ejecutable por sí mismo y módulo silencioso para los demás.
En el segundo, el cambio importante no es estético. Una función que imprime no se puede probar sin capturar la salida, y no se puede reutilizar para generar un PDF o una respuesta HTTP. Una función que devuelve un diccionario sí. La regla general es: las funciones calculan y devuelven; el código de nivel superior, o el guardián, decide qué hacer con el resultado.
Para reproducir el proyecto en otra máquina, el orden es siempre el mismo: python3 -m venv .venv, activar, pip install -r requirements.txt. Si el archivo no existe todavía, pip freeze > requirements.txt lo genera después de instalar. Fijar versiones evita que una actualización silenciosa cambie los resultados de tu modelo.
Resumen
- Un módulo es un archivo
.py; un paquete es una carpeta con__init__.py. import xtrae el módulo;from x import ytrae un nombre;asrenombra.- Python busca en
sys.pathen orden y reutiliza lo que ya está ensys.modules. - Un módulo se ejecuta una sola vez, en su primera importación.
__name__vale"__main__"en el archivo ejecutado y el nombre del módulo al importarlo.- Los imports relativos (
from .modulo import x) requieren ejecutar conpython3 -m paquete.modulo. - Un entorno virtual aísla dependencias;
requirements.txtlas reproduce con versiones fijas.
Evaluación
Importas el mismo módulo en dos archivos distintos del programa. ¿Qué ocurre?
¿Qué imprime print(__name__) cuando ejecutas python3 conversiones.py directamente?
¿Qué problema resuelve un entorno virtual?
¿Ya lo entendiste?
Márcala como completada para llevar tu progreso y desbloquear el siguiente paso de la ruta.