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

  1. Qué es un módulo (y por qué ya usas módulos)
  2. import, from ... import y alias con as
  3. Crear papyrus_utils.py, el módulo de la tienda
  4. if __name__ == "__main__": módulo y script a la vez
  5. Paquetes: carpetas con __init__.py
  6. 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

print(math.sqrt(16))    # 4.0
print(math.pi)          # 3.141592653589793

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 import van 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 None

Observa dos decisiones de diseño:

  1. El módulo también tiene docstring: la primera cadena del archivo documenta el módulo entero (help(papyrus_utils) la muestra).
  2. buscar_libro() ha cambiado de firma: en la lección 03-01 leía la lista global catalogo de menu.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:

from papyrus_utils import precio_final, buscar_libro, NOMBRE_TIENDA

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')}")   # 2
flowchart 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__.py puede 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:

  1. La carpeta del script que estás ejecutando — por eso menu.py encuentra papyrus_utils.py: son vecinos.
  2. Las carpetas de la biblioteca estándar — ahí viven math, random, datetime.
  3. El site-packages del entorno activo — ahí instala pip; con el .venv activado, es el site-packages del 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:

import sys
print(sys.path)     # la lista de carpetas, en orden de búsqueda

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.py de pruebas y import math cargará 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 .py no está junto al script. Comprueba dónde estás (pwd en la terminal) y dónde está el archivo.
  • Eclipsar módulos estándar: un random.py propio rompe import random en toda la carpeta. Si un módulo estándar "se comporta raro", busca archivos tuyos con su nombre.
  • Escribir import papyrus_utils.py: el import usa el nombre del módulo, sin extensión. Con .py obtienes un error de módulo py inexistente.
  • Código suelto en el módulo: un print() fuera de funciones y fuera del bloque __main__ se ejecutará en cada primer import, 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

Módulo 2: Estructuras de Control

Módulo 3: Funciones y Módulos

Módulo 4: Estructuras de Datos

Módulo 5: Programación Orientada a Objetos

Módulo 6: Manejo de Archivos

Módulo 7: Manejo de Errores y Excepciones

Módulo 8: Temas Avanzados

Módulo 9: Pruebas y Depuración

Módulo 10: Desarrollo Web con Python

Módulo 11: Ciencia de Datos con Python

Módulo 12: Proyecto Final

© Copyright 2026. Todos los derechos reservados