Un módulo es, en su forma más simple, un archivo .py normal. Nada más. Si tienes un archivo llamado utils.py, ya tienes un módulo. La magia está en lo que Python hace cuando escribes import utils.
En el momento en que Python procesa esa instrucción, ejecuta el archivo de arriba a abajo, como si lo corrieras directamente con python utils.py, y guarda el resultado en un objeto módulo. Ese objeto tiene atributos que corresponden a cada nombre que quedó definido en el archivo: funciones, clases, variables, lo que sea. Cuando luego escribes utils.calculate(x), Python simplemente accede al atributo calculate de ese objeto.
El detalle que más sorprende a gente nueva es que esa ejecución ocurre exactamente una vez, sin importar cuántas veces escribas import utils en tu código. Python mantiene un diccionario llamado sys.modules que actúa como caché: la primera vez que importas un módulo, lo ejecuta y guarda el objeto resultante ahí; las veces siguientes, simplemente devuelve lo que ya tenía en caché. Esto es intencional: re-ejecutar un módulo en cada import sería costoso y llenaría de efectos secundarios impredecibles cualquier programa mediano.
Esto tiene una implicación práctica importante: si un módulo tiene código que imprime algo, conecta a una base de datos, o modifica estado global al cargarse, ese código corre una sola vez, en el primer import. Las importaciones siguientes son básicamente gratuitas.
¿Cuándo importa todo esto? Cuando depuras comportamientos raros en sesiones interactivas, cuando escribes código que necesitas recargar sin reiniciar el intérprete (desarrollo de plugins, por ejemplo), o cuando entiendes por qué dos partes de tu programa ven exactamente el mismo objeto al importar el mismo módulo.
Si te saltas sys.modules y no entiendes la ejecución única, puedes pasar horas buscando por qué un cambio en un archivo no se refleja, o por qué un efecto secundario ocurre más veces de las esperadas.
# greetings.py ← este es el módulo que vamos a importar
print("Módulo greetings cargado") # corre solo una vez, al primer import
DEFAULT_LANG = "es"
def hello(name: str, lang: str = DEFAULT_LANG) -> str:
messages = {
"es": f"Hola, {name}",
"en": f"Hello, {name}",
}
return messages.get(lang, f"Hi, {name}")
class Greeter:
def __init__(self, lang: str = DEFAULT_LANG):
self.lang = lang
def greet(self, name: str) -> str:
return hello(name, self.lang)
# main.py ← aquí exploramos qué pasa al importar
import sys
import importlib
# Primer import: Python ejecuta greetings.py de arriba a abajo.
# Verás "Módulo greetings cargado" impreso exactamente aquí.
import greetings
# Segundo import del mismo módulo: no pasa nada, Python usa la caché.
import greetings # ← sin print, sin re-ejecución
# El objeto módulo vive en sys.modules con su nombre como clave.
cached = sys.modules["greetings"]
print(cached is greetings) # True: es literalmente el mismo objeto
# Acceder a atributos del módulo es acceder a nombres definidos en el archivo.
print(greetings.DEFAULT_LANG) # "es"
print(greetings.hello("Ana")) # "Hola, Ana"
g = greetings.Greeter(lang="en")
print(g.greet("Ana")) # "Hello, Ana"
# Simulamos un cambio en tiempo de ejecución (típico en desarrollo interactivo).
greetings.DEFAULT_LANG = "en"
print(greetings.DEFAULT_LANG) # "en" — modificamos el objeto módulo directamente
# importlib.reload() re-ejecuta el archivo desde disco y reconstruye el objeto.
# Útil cuando cambias el .py sin reiniciar el intérprete.
importlib.reload(greetings) # vuelves a ver "Módulo greetings cargado"
print(greetings.DEFAULT_LANG) # "es" de nuevo — el reload pisó nuestra modificación
Desglose del código
greetings.py es intencionalmente variado: tiene un print suelto, una variable, una función y una clase. Eso ilustra que Python no distingue: todo lo que está a nivel de módulo se ejecuta al importar, y todo nombre que quede definido al terminar se convierte en atributo del objeto módulo.
En main.py, el doble import greetings confirma visualmente que el print solo aparece una vez. No es que Python ignore la segunda línea; la procesa, busca "greetings" en sys.modules, lo encuentra y devuelve lo que ya tenía. El módulo nunca se toca de nuevo.
La línea cached is greetings usa is (identidad, no igualdad) para demostrar que no hay copia: ambas referencias apuntan al mismo objeto en memoria. Esto explica por qué si un módulo guarda estado mutable, todos los que lo importan comparten ese estado.
La modificación de DEFAULT_LANG directamente en el objeto módulo es válida Python, aunque rara en producción. Lo interesante es ver que importlib.reload() la borra, porque reload vuelve a ejecutar el archivo y reescribe los atributos del objeto módulo con los valores originales. No crea un objeto nuevo (la referencia en sys.modules sigue siendo la misma), solo repopula sus atributos.
Errores que debes conocer
Error: asumir que reload() actualiza referencias que ya se extrajeron del módulo antes del reload.
# ❌ Wrong
from greetings import DEFAULT_LANG # extrae el valor "es" como variable local
importlib.reload(greetings) # recarga el módulo
print(DEFAULT_LANG) # sigue siendo "es" aunque hubieras cambiado el archivo
# — peor aún, si lo cambiaste a "fr", no se refleja aquí
# ✅ Right
import greetings
importlib.reload(greetings)
print(greetings.DEFAULT_LANG) # ahora sí lees del objeto módulo recargado
from módulo import nombre copia el valor en una variable local en el momento de la importación. El reload actualiza el objeto módulo, pero tu variable local ya no tiene conexión con él.
Error: poner lógica costosa o con efectos secundarios a nivel de módulo sin protegerla, esperando que no se ejecute al importar.
# ❌ Wrong
# config.py
import time
time.sleep(5) # bloquea 5 segundos cada vez que alguien importa config
DB_URL = "postgresql://..."
# ✅ Right
# config.py
DB_URL = "postgresql://..."
def initialize(): # el código pesado solo corre cuando alguien lo llama explícitamente
import time
time.sleep(5)
Mover el código pesado dentro de una función deja que el módulo se importe instantáneamente; quien necesite inicializar llama a initialize() cuando corresponde.
N° 63