El diagnóstico al cerrar la lección anterior era claro: precio_final() está copiada en tarifa.py y en otros scripts, buscar_libro() vive encerrada en menu.py, y cada copia es una fuente de inconsistencias — el día que Ana cambie el redondeo, ¿en cuántos sitios habrá que tocarlo? La solución es el módulo: un archivo .py cuyas funciones y constantes pueden importarse desde cualquier otro archivo. De hecho, ya consumes módulos sin saberlo cada vez que instalabas paquetes con pip (lección 01-06); hoy aprenderás la otra mitad: crear los tuyos. Construiremos papyrus_utils.py, el primer módulo propio de la tienda, entenderás las variantes de import, el idioma if __name__ == "__main__", qué es un paquete con __init__.py y dónde busca Python los módulos que le pides.
Contenido
- Qué es un módulo (y por qué ya usas módulos)
import,from ... importy alias conas- Crear
papyrus_utils.py, el módulo de la tienda if __name__ == "__main__": módulo y script a la vez- Paquetes: carpetas con
__init__.py - Dónde busca Python los módulos:
sys.path
Qué es un módulo (y por qué ya usas módulos)
Un módulo es, simplemente, un archivo .py. Todo lo que define en su nivel superior — funciones, constantes, variables — puede ponerse a disposición de otros archivos mediante import. Los módulos vienen de tres orígenes:
| Origen | Ejemplos | ¿Hay que instalarlos? |
|---|---|---|
| Biblioteca estándar (vienen con Python) | math, random, datetime |
No |
De terceros (se instalan con pip en el .venv) |
requests, pandas |
Sí (lección 01-06) |
| Propios (los escribes tú) | papyrus_utils |
No: basta un archivo .py |
La mecánica de importación es idéntica para los tres. Empecemos con uno estándar para ver la sintaxis con terreno conocido:
import math carga el módulo y lo deja disponible bajo su nombre; a partir de ahí, todo lo suyo se usa con la notación de punto: math.sqrt, math.pi. El nombre del módulo actúa de "apellido" que evita colisiones con tus propias variables.
import, from ... import y alias con as
Hay tres variantes principales, cada una con su caso de uso:
# 1) Importar el módulo entero (se usa con prefijo)
import math
math.sqrt(16)
# 2) Importar nombres concretos (se usan sin prefijo)
from math import sqrt, pi
sqrt(16)
# 3) Alias: renombrar el módulo (o el nombre) al importarlo
import datetime as dt
dt.date.today()| Variante | Ventaja | Inconveniente | Cuándo usarla |
|---|---|---|---|
import modulo |
Queda claro de dónde viene cada cosa | Más tecleo (modulo.funcion) |
Por defecto; siempre que dudes |
from modulo import nombre |
Llamadas cortas | Se pierde el "apellido"; riesgo de colisión de nombres | Pocos nombres, muy usados en el archivo |
import modulo as alias |
Abrevia módulos de nombre largo | Un alias caprichoso despista | Alias consolidados por convención (numpy as np, módulo 11) |
Existe una cuarta forma, from modulo import * (importar todo sin prefijo), que debes conocer para no usarla: llena tu archivo de nombres invisibles — no sabes qué importaste ni qué pisaste — y PEP 8 la reserva para casos muy contados.
Dos convenciones importantes de PEP 8:
- Los
importvan al principio del archivo, tras la docstring del módulo, uno por línea. - Orden en tres bloques separados por línea en blanco: biblioteca estándar → paquetes de terceros → módulos propios.
Crear papyrus_utils.py, el módulo de la tienda
Manos a la obra. En la carpeta papyrus (junto a menu.py), crea papyrus_utils.py con las piezas que veníamos copiando de script en script:
"""Utilidades compartidas de la librería Papyrus.
Constantes de la tienda y funciones de precios y búsqueda
usadas por menu.py, ticket.py y venta_socio.py.
"""
NOMBRE_TIENDA = "Papyrus"
IVA_LIBROS = 0.04
DESCUENTO_SOCIO = 0.05
def precio_final(base, socio=False, iva=IVA_LIBROS):
"""Devuelve el precio final: descuento de socio (si procede) y luego IVA."""
if socio:
base = base * (1 - DESCUENTO_SOCIO)
return round(base * (1 + iva), 2)
def buscar_libro(catalogo, buscado):
"""Devuelve el índice del título en el catálogo, o None si no está.
La comparación ignora mayúsculas y minúsculas.
"""
for i, titulo in enumerate(catalogo):
if titulo.lower() == buscado.lower():
return i
return NoneObserva dos decisiones de diseño:
- El módulo también tiene docstring: la primera cadena del archivo documenta el módulo entero (
help(papyrus_utils)la muestra). buscar_libro()ha cambiado de firma: en la lección 03-01 leía la lista globalcatalogodemenu.py; eso funcionaba porque función y lista vivían en el mismo archivo. Un módulo no puede (ni debe) depender de las variables de quien lo importa, así que ahora recibe el catálogo como parámetro — la lección de "recibe datos, no leas globales" (03-01) aplicada en serio. Las constantes (IVA_LIBROS,DESCUENTO_SOCIO) sí viven en el módulo: son de la tienda, no de un script concreto.
Y ahora, la recompensa. Cualquier script de la carpeta puede escribir:
import papyrus_utils
catalogo = ["La Odisea", "Hamlet", "El Quijote"]
precios = [12.50, 9.95, 15.90]
indice = papyrus_utils.buscar_libro(catalogo, "hamlet")
if indice is not None:
precio_luis = papyrus_utils.precio_final(precios[indice], socio=True)
print(f"{catalogo[indice]} para Luis: {precio_luis} €") # Hamlet para Luis: 9.83 €O, si prefiere nombres cortos:
Una sola definición, tantos usuarios como haga falta. El día que cambie el IVA, se edita una línea de un archivo y todos los scripts quedan actualizados. En la próxima lección reescribiremos menu.py sobre este módulo.
Un detalle técnico útil: Python ejecuta el archivo del módulo una sola vez, en el primer import del programa; los siguientes import reutilizan el módulo ya cargado. Por eso importar es barato y por eso no debes poner "código de trabajo" suelto en un módulo... lo que nos lleva al siguiente apartado.
if __name__ == "__main__": módulo y script a la vez
Al importar, Python ejecuta el archivo entero: los def definen funciones (inofensivo), pero un print() o un input() sueltos se ejecutarían también, colándose en el programa de quien importa. ¿Cómo tener un módulo que además pueda ejecutarse solo — por ejemplo, para probarlo? Python asigna a cada archivo una variable automática, __name__:
- Si el archivo se ejecuta directamente (
python papyrus_utils.py),__name__vale"__main__". - Si el archivo se importa,
__name__vale su nombre de módulo ("papyrus_utils").
El idioma estándar aprovecha esa diferencia. Añade al final de papyrus_utils.py:
if __name__ == "__main__":
# Pequeña demostración: solo corre al ejecutar el archivo directamente.
print(f"Módulo de utilidades de {NOMBRE_TIENDA}")
print(f"Hamlet, socio: {precio_final(9.95, socio=True)} €") # 9.83 €
demo = ["La Odisea", "Hamlet", "El Quijote"]
print(f"Índice de 'el quijote': {buscar_libro(demo, 'el quijote')}") # 2flowchart TD
A[Python ejecuta papyrus_utils.py] --> B{"¿__name__?"}
B -->|"python papyrus_utils.py<br/>__name__ == '__main__'"| C[Corre la demo/pruebas]
B -->|"import papyrus_utils<br/>__name__ == 'papyrus_utils'"| D[Solo define funciones y constantes:<br/>silencio absoluto]
Resultado: python papyrus_utils.py imprime la demostración; import papyrus_utils desde menu.py no imprime nada. Acostúmbrate a cerrar así también tus scripts "principales": es la marca de fábrica del código Python bien educado, y en el módulo 9 este bloque será el trampolín natural de las pruebas.
Paquetes: carpetas con __init__.py
Cuando los módulos se multiplican, se agrupan en paquetes: carpetas que contienen módulos y un archivo especial __init__.py (habitualmente vacío) que le dice a Python "esta carpeta es un paquete". Si Papyrus sigue creciendo, su proyecto podría organizarse así:
papyrus/ <- carpeta del proyecto (con su .venv)
menu.py <- script principal
papyrus_utils.py <- nuestro módulo (etapa actual)
tienda/ <- un PAQUETE (etapa futura)
__init__.py <- marca la carpeta como paquete
precios.py <- módulo: precio_final, IVA_LIBROS...
inventario.py <- módulo: buscar_libro, stocks...La notación de punto atraviesa los niveles:
import tienda.precios
tienda.precios.precio_final(9.95, socio=True)
from tienda.inventario import buscar_libro
buscar_libro(catalogo, "Fausto")__init__.pypuede estar vacío; también puede ejecutar código de inicialización del paquete o reexportar nombres cómodos — de momento, vacío es perfecto.- Los paquetes pueden anidarse (
tienda.informes.mensual), formando el árbol que ya has visto en bibliotecas de terceros.
Para el tamaño actual de Papyrus, un único papyrus_utils.py plano es la decisión correcta — crear paquetes prematuramente es burocracia. Pero ahora, cuando en los módulos 10 y 11 te encuentres con from flask import Flask o import pandas as pd, sabrás exactamente qué estructura hay detrás: carpetas con __init__.py y archivos .py.
Dónde busca Python los módulos: sys.path
Cuando escribes import papyrus_utils, ¿cómo encuentra Python el archivo? Recorre, en orden, una lista de carpetas llamada sys.path:
- La carpeta del script que estás ejecutando — por eso
menu.pyencuentrapapyrus_utils.py: son vecinos. - Las carpetas de la biblioteca estándar — ahí viven
math,random,datetime. - El
site-packagesdel entorno activo — ahí instala pip; con el.venvactivado, es elsite-packagesdel proyecto (lección 01-06, ahora con sentido completo).
Usa el primer módulo que encuentre y, si agota la lista, lanza ModuleNotFoundError. Puedes inspeccionar la lista tú mismo:
Dos consecuencias prácticas del orden de búsqueda:
- Si tu script y tu módulo están en la misma carpeta, todo funciona sin configurar nada. Es nuestro caso y el de la mayoría de proyectos pequeños.
- Como tu carpeta va primera, un archivo tuyo llamado como un módulo estándar lo eclipsa: crea un
math.pyde pruebas yimport mathcargará el tuyo, rompiendo cosas con errores desconcertantes. Nunca nombres tus archivos como módulos conocidos (random.py,string.py,test.py...).
Errores Comunes y Consejos
ModuleNotFoundError: No module named 'papyrus_utils': casi siempre significa que ejecutas el script desde otra carpeta o que el.pyno está junto al script. Comprueba dónde estás (pwden la terminal) y dónde está el archivo.- Eclipsar módulos estándar: un
random.pypropio rompeimport randomen toda la carpeta. Si un módulo estándar "se comporta raro", busca archivos tuyos con su nombre. - Escribir
import papyrus_utils.py: elimportusa el nombre del módulo, sin extensión. Con.pyobtienes un error de módulopyinexistente. - Código suelto en el módulo: un
print()fuera de funciones y fuera del bloque__main__se ejecutará en cada primerimport, ensuciando a quien importa. Todo lo ejecutable, dentro de funciones o del bloque__main__. from modulo import *: contamina tu espacio de nombres y hace imposible saber de dónde salió cada función. Importa nombres concretos o el módulo entero.- Modificar el módulo y no ver los cambios en el REPL: el REPL cachea los módulos importados; reinícialo tras editar el archivo (los scripts no sufren esto: cada ejecución parte de cero).
- Consejo: pregunta clave antes de mover una función a un módulo: "¿la necesitan dos o más scripts, o podría necesitarla mañana?". Si sí, al módulo. Constantes del negocio (
IVA_LIBROS) incluidas.
Ejercicios
Ejercicio 1: venta_socio.py renace sobre el módulo
Reescribe venta_socio.py para que no defina ninguna función ni constante propia: debe importar de papyrus_utils lo necesario, pedir con input() un título, buscarlo en catalogo = ["La Odisea", "Hamlet", "El Quijote"] (con precios = [12.50, 9.95, 15.90]), e imprimir el precio de socio de Luis o '<título>' no está en el catálogo. si no existe. Usa la variante from ... import.
Ejercicio 2: el módulo autoejecutable
Añade a papyrus_utils.py la función es_socio_valido(codigo) que devuelva True si codigo es una cadena de exactamente 4 dígitos (pista: len() y el método isdigit() que conoces del módulo 2). Amplía el bloque if __name__ == "__main__" con tres comprobaciones que impriman el resultado de es_socio_valido("0042"), es_socio_valido("42") y es_socio_valido("ABCD"). Verifica que python papyrus_utils.py muestra las comprobaciones y que importarlo desde otro script no muestra nada.
Ejercicio 3: anatomía de un paquete
Sin escribir código, responde razonadamente: (a) ¿qué convierte una carpeta corriente en un paquete?; (b) si Papyrus tuviera la estructura tienda/precios.py con __init__.py, ¿qué dos formas de import permitirían llamar a precio_final?; (c) ¿en qué orden buscará Python el módulo tienda cuando menu.py haga import tienda, y por qué lo encuentra?
Soluciones
Ejercicio 1:
from papyrus_utils import precio_final, buscar_libro
catalogo = ["La Odisea", "Hamlet", "El Quijote"]
precios = [12.50, 9.95, 15.90]
buscado = input("Título para Luis: ")
indice = buscar_libro(catalogo, buscado)
if indice is None:
print(f"'{buscado}' no está en el catálogo.")
else:
print(f"'{catalogo[indice]}' para Luis: {precio_final(precios[indice], socio=True)} €")Todo el conocimiento del negocio (IVA, descuento, cómo comparar títulos) vive en el módulo; el script solo orquesta entrada, llamada y salida. Prueba con hamlet: 'Hamlet' para Luis: 9.83 €.
Ejercicio 2:
def es_socio_valido(codigo):
"""Devuelve True si el código de socio son exactamente 4 dígitos."""
return len(codigo) == 4 and codigo.isdigit()
if __name__ == "__main__":
print(f"Módulo de utilidades de {NOMBRE_TIENDA}")
print(f"Hamlet, socio: {precio_final(9.95, socio=True)} €")
print(es_socio_valido("0042")) # True
print(es_socio_valido("42")) # False (longitud 2)
print(es_socio_valido("ABCD")) # False (no son dígitos)El and cortocircuita (módulo 2): si la longitud no es 4, ni siquiera evalúa isdigit(). Al importar el módulo desde otro archivo, el bloque __main__ queda en silencio porque __name__ vale "papyrus_utils".
Ejercicio 3: (a) Contener un archivo __init__.py (aunque esté vacío). (b) import tienda.precios y llamar tienda.precios.precio_final(...), o from tienda.precios import precio_final y llamar precio_final(...) directamente. (c) Python recorre sys.path en orden: primero la carpeta del script (papyrus/), donde encuentra la carpeta tienda con su __init__.py; no llega a mirar la biblioteca estándar ni site-packages.
Conclusión
Papyrus tiene por fin una caja de herramientas central: papyrus_utils.py, con las constantes de la tienda, precio_final(base, socio=False, iva=IVA_LIBROS) y una buscar_libro(catalogo, buscado) que ya no depende de globales ajenas — y con un bloque if __name__ == "__main__" que la hace probable con solo ejecutarla. Sabes elegir entre import modulo, from modulo import nombre y los alias as, sabes qué es un paquete (__init__.py) y entiendes el mapa de carpetas que recorre sys.path para resolver cada import. Ese mismo mecanismo abre una puerta enorme: la biblioteca estándar, decenas de módulos ya escritos, probados y documentados que vienen con Python — matemáticas, azar, fechas, sistema... En la próxima lección haremos una visita guiada por los más útiles y cerraremos el módulo reescribiendo menu.py como se merece: importando papyrus_utils.
Curso de Programación en Python
Módulo 1: Introducción a Python
- Introducción a Python
- Configuración del Entorno de Desarrollo
- Sintaxis de Python y Tipos de Datos Básicos
- Variables y Constantes
- Entrada y Salida Básica
- Entornos Virtuales y Gestión de Paquetes
Módulo 2: Estructuras de Control
- Sentencias Condicionales
- Bucles: for y while
- Herramientas de Control de Flujo
- Comprensiones de Listas
Módulo 3: Funciones y Módulos
- Definición de Funciones
- Argumentos de Función
- Funciones Lambda
- Módulos y Paquetes
- Visión General de la Biblioteca Estándar
Módulo 4: Estructuras de Datos
Módulo 5: Programación Orientada a Objetos
Módulo 6: Manejo de Archivos
- Lectura y Escritura de Archivos
- Trabajo con Archivos CSV
- Manejo de Datos JSON
- Operaciones con Archivos y Directorios
Módulo 7: Manejo de Errores y Excepciones
- Introducción a las Excepciones
- Manejo de Excepciones
- Lanzamiento de Excepciones
- Excepciones Personalizadas
- Buenas Prácticas y Registro de Errores con logging
Módulo 8: Temas Avanzados
- Anotaciones de Tipos (type hints)
- Decoradores
- Generadores
- Administradores de Contexto
- Concurrencia: Hilos y Procesos
- Asyncio para Programación Asíncrona
Módulo 9: Pruebas y Depuración
- Introducción a las Pruebas
- Pruebas Unitarias con unittest
- Pruebas con pytest
- Desarrollo Guiado por Pruebas
- Técnicas de Depuración
- Uso de pdb para Depuración
Módulo 10: Desarrollo Web con Python
- Introducción al Desarrollo Web
- Fundamentos del Framework Flask
- Construcción de APIs REST con Flask
- Introducción a Django
- Construcción de Aplicaciones Web con Django
Módulo 11: Ciencia de Datos con Python
- Introducción a la Ciencia de Datos
- NumPy para Computación Numérica
- Pandas para Manipulación de Datos
- Matplotlib para Visualización de Datos
- Introducción al Aprendizaje Automático con scikit-learn
