Módulos y paquetes en Python — ejercicios con temperaturas, divisas y una tienda (y el precio con IVA que salía con descuento)
Quería imprimir un precio con IVA y ese mismo precio con descuento. Escribí dos módulos, impuestos.py y descuentos.py, cada uno con una función aplica, y los importé con from … import aplica. Lancé el programa y la salida decía que 100 € con IVA eran 90 €. Revisé la función del IVA: estaba bien. Revisé el 1,21: estaba bien. No había ningún error, ninguna excepción. El problema no estaba en lo que calculaba, sino en **cuál de las dos aplica estaba llamando**.
Los módulos tienen un puñado de trampas así, silenciosas y que no dan error. Hoy practicamos con tres programas de dificultad creciente (el último, una tienda con paquete y subpaquete), tres fragmentos para depurar y un chuletario para llevarte.
Estos ejercicios usan la teoría de módulos y paquetes y repasan lo de la práctica. Sin ellas puedes hacerlos, pero te costará más.
Tabla de Contenidos
Cómo usar este artículo
- Lee el enunciado de un nivel y no mires la solución todavía.
- Escribe tu versión, ejecútala y compara tu salida con la esperada.
- Si te atascas, mira solo la 🔵 pista y vuelve a intentarlo.
- Cuando termines, compara con la solución y fíjate en por qué está hecha así, no solo en que dé la misma salida.
Ningún ejercicio usa input(). Los datos van fijos en el código para que tu salida sea idéntica a la esperada. Crea todos los archivos de un ejercicio en la misma carpeta y lanza el programa desde ella.
Nivel 1 · Básico: un módulo de temperaturas
Enunciado. Crea un módulo temperatura.py con una constante CERO_ABSOLUTO_C igual a -273.15 y dos funciones: c_a_f(c), que convierte grados Celsius a Fahrenheit, y c_a_k(c), que los convierte a Kelvin. Después, en programa.py, importa el módulo con alias (temp) para llamar a c_a_f, importa c_a_k directamente con from y muestra los tres resultados de la salida esperada.
Salida esperada:
20 °C en °F: 68.0 20 °C en K: 293.15 Cero absoluto: -273.15 °C
Fahrenheit = Celsius × 9 / 5 + 32. Kelvin = Celsius − cero absoluto (restar un negativo suma). Con import temperatura as temp llamas a las funciones como temp.c_a_f(...); con from temperatura import c_a_k llamas a c_a_k(...) sin prefijo.
Nivel 2 · Intermedio: dos módulos con el mismo nombre de función
Enunciado. Crea dos módulos, euros.py y dolares.py, con una función formatea(cantidad) cada uno. La de euros devuelve el importe con dos decimales y el símbolo detrás (19.50 €) y la de dólares, con el símbolo delante ($19.50). En programa.py importa las dos funciones sin que se pisen y muestra un mismo precio en ambas monedas; añade una tercera línea que use euros.formatea con el prefijo.
Además, euros.py debe poder lanzarse directamente para probarse: dentro de if __name__ == "__main__": imprime formatea(5) y formatea(19.5). **Esas dos líneas no pueden aparecer cuando programa.py importa el módulo.**
Salida esperada de python3 programa.py:
Precio: 19.50 € / $19.50 Sin alias: 19.50 €
Salida esperada de python3 euros.py:
5.00 € 19.50 €
Dos from … import formatea seguidos se pisan. Cambia el nombre con as: from euros import formatea as en_euros. Para el formato, un f-string con dos decimales: f"{cantidad:.2f} €".
Nivel 3 · Desafío: una tienda con paquete y subpaquete
Enunciado. Monta un paquete tienda con esta estructura:
programa.py
tienda/
├── __init__.py
├── precios.py
├── ticket.py
└── descuentos/
├── __init__.py
├── porcentaje.py
└── umbral.py
precios.py: una constanteIVA = 0.21ycon_iva(base), que devuelve el precio con IVA redondeado a dos decimales.descuentos/porcentaje.py:aplica(precio, pct), que rebaja el precio unpctpor ciento (redondeado a dos decimales).descuentos/umbral.py:pct_por_importe(importe), que devuelve10si el importe es 100 o más, y0si no.ticket.py:total(base), que calcula el precio con IVA, mira qué descuento corresponde a ese precio y devuelve el total final. Debe usar las otras tres funciones.
Prueba el paquete desde programa.py, que debe estar fuera de tienda.
Salida esperada:
Compra de 50 €: 60.5 € Compra de 100 €: 108.9 €
Dentro del paquete, los módulos se importan con la ruta completa desde la carpeta de arriba: from tienda.precios import con_iva. El descuento se decide con el precio ya con IVA: 50 € de base son 60,50 € (no llega a 100, sin descuento) y 100 € son 121 € (con un 10 %). Y recuerda los dos __init__.py.
¿Qué falla aquí?
Tres fragmentos con un fallo cada uno. Antes de leer la explicación, haz dos cosas: predice si da error y, si no lo da, qué imprime.
Fragmento 1
precios.py
IVA = 0.21
def con_iva(base):
return round(base * (1 + IVA), 2)
print("Probando con_iva(100):", con_iva(100))
programa.py
import precios
print("Total:", precios.con_iva(50))
*Piénsalo antes de seguir: ¿qué imprime python3 programa.py? Solo quiero ver el total de 50 €.*
Explicación. No hay ningún error, pero la salida trae una línea que nadie ha pedido:
Probando con_iva(100): 121.0 Total: 60.5
import precios ejecuta precios.py de arriba abajo, y el print de prueba está en el nivel superior del módulo, así que se ejecuta también al importarlo. No es un fallo de la función: es código que no debía ejecutarse al importar.
Dejar pruebas o print sueltos al final de un módulo. Cada programa que lo importe los ejecutará, y en el peor caso tendrás una prueba que falla (o que pide datos) antes de que empiece tu programa.
Arreglo. Mete la prueba bajo if __name__ == "__main__": para que solo corra al lanzar precios.py directamente:
if __name__ == "__main__":
print("Probando con_iva(100):", con_iva(100))
Total: 60.5
Al nivel superior de un módulo deja solo definiciones (constantes, funciones, clases). Todo lo demás, bajo el if.
Fragmento 2
impuestos.py
def aplica(precio):
return round(precio * 1.21, 2)
descuentos.py
def aplica(precio):
return round(precio * 0.90, 2)
programa.py
from impuestos import aplica
from descuentos import aplica
print("Con IVA:", aplica(100))
print("Con descuento:", aplica(100))
¿Da error? ¿Qué imprime? Quiero ver 100 € con IVA y 100 € con descuento.
Explicación. No hay error ni excepción, y por eso es tan peligroso:
Con IVA: 90.0 Con descuento: 90.0
Las dos líneas usan el descuento. El segundo from descuentos import aplica sustituyó al nombre aplica que había traído el primero, y a partir de ahí aplica apunta a la función de los descuentos para siempre. La del IVA sigue existiendo en el módulo impuestos, pero ya no tienes forma de llamarla por ese nombre.
Traer con from … import el mismo nombre de dos módulos. No avisa, y el último gana. Con dos funciones que reciben los mismos parámetros ni siquiera ves un TypeError: solo números equivocados.
Arreglo. Importa los módulos enteros y usa el prefijo (o dale un alias a cada función):
import impuestos
import descuentos
print("Con IVA:", impuestos.aplica(100))
print("Con descuento:", descuentos.aplica(100))
Con IVA: 121.0 Con descuento: 90.0
Si ves en tu programa dos from … import con el mismo nombre al final, es una señal de alarma. El prefijo existe justo para esto.
Fragmento 3
Usamos el paquete tienda del Nivel 3.
f3.py
import tienda print(tienda.precios.con_iva(100))
¿Da error? ¿Qué imprime? Quiero el precio con IVA de 100 €, que debería ser 121.
Explicación. Se ejecuta, pero se detiene con un error de atributo:
Traceback (most recent call last):
File "f3.py", line 3, in <module>
print(tienda.precios.con_iva(100))
^^^^^^^^^^^^^^
AttributeError: module 'tienda' has no attribute 'precios'
import tienda carga el paquete (ejecuta su __init__.py, que está vacío), pero eso no importa sus módulos. Para Python, tienda no tiene ningún atributo precios hasta que alguien importe el módulo tienda.precios. No es que la ruta esté mal; es que nadie ha importado ese módulo todavía.
Esperar que import paquete te deje usar paquete.modulo. Importar un paquete no importa sus módulos.
Arreglo. Importa el módulo concreto, con la ruta completa:
import tienda.precios print(tienda.precios.con_iva(100))
121.0
import paquete.modulo o from paquete import modulo. Las dos dejan el módulo listo para usar; cuál elegir es cuestión de cómo quieras escribirlo después.
Soluciones
Mira esta sección solo cuando hayas intentado el ejercicio. Si tu código es distinto pero da la misma salida y cumple los requisitos, también es una solución válida.
Solución del Nivel 1
temperatura.py
CERO_ABSOLUTO_C = -273.15
def c_a_f(c):
"""Grados Celsius a Fahrenheit."""
return c * 9 / 5 + 32
def c_a_k(c):
"""Grados Celsius a Kelvin."""
return c - CERO_ABSOLUTO_C
programa.py
import temperatura as temp
from temperatura import c_a_k
print("20 °C en °F:", temp.c_a_f(20))
print("20 °C en K:", c_a_k(20))
print("Cero absoluto:", temp.CERO_ABSOLUTO_C, "°C")
Por qué está hecho así:
- El módulo solo define (una constante y dos funciones) y no imprime nada: así cualquier programa puede importarlo sin efectos secundarios.
import temperatura as temple da un alias corto al módulo; con él llamamos atemp.c_a_f(20)y leemostemp.CERO_ABSOLUTO_C. Todo sigue con prefijo.from temperatura import c_a_ktrae solo esa función, que usamos sin prefijo. En el mismo programa conviven las dos formas, y no se estorban.c - CERO_ABSOLUTO_Csuma 273,15 porque la constante es negativa. Si hubieras escritoc + 273.15a mano, el resultado sería el mismo, pero un número «mágico» suelto es peor que una constante con nombre.- Comprueba: 20 × 9 / 5 + 32 = 68, y 20 + 273,15 = 293,15.
Solución del Nivel 2
euros.py
def formatea(cantidad):
return f"{cantidad:.2f} €"
if __name__ == "__main__":
print(formatea(5))
print(formatea(19.5))
dolares.py
def formatea(cantidad):
return f"${cantidad:.2f}"
programa.py
from euros import formatea as en_euros
from dolares import formatea as en_dolares
import euros
precio = 19.5
print("Precio:", en_euros(precio), "/", en_dolares(precio))
print("Sin alias:", euros.formatea(precio))
Por qué está hecho así:
- Las dos funciones se llaman
formateay no hay conflicto porque viven en módulos distintos. El conflicto solo aparecería al traerlas confromsin alias, y por eso usamosas. en_eurosyen_dolaresson etiquetas para dos funciones diferentes: dicen en el propio programa qué hace cada una, algo queformateaa secas no decía.- Además de los alias,
programa.pyhaceimport eurospara usar el prefijo en la última línea. Puedes importar de un mismo módulo de varias formas a la vez. - El bloque
if __name__ == "__main__":solo se ejecuta conpython3 euros.py(segunda salida). Al importareurosdesdeprograma.py,__name__vale"euros"y el bloque se salta (primera salida). :.2ffuerza dos decimales, de ahí5.00 €.
Solución del Nivel 3
tienda/precios.py
IVA = 0.21
def con_iva(base):
"""Precio con IVA, redondeado a céntimos."""
return round(base * (1 + IVA), 2)
tienda/descuentos/porcentaje.py
def aplica(precio, pct):
"""Rebaja el precio un pct por ciento."""
return round(precio * (1 - pct / 100), 2)
tienda/descuentos/umbral.py
def pct_por_importe(importe):
"""10 % de descuento a partir de 100 euros."""
return 10 if importe >= 100 else 0
tienda/ticket.py
from tienda.precios import con_iva
from tienda.descuentos.porcentaje import aplica
from tienda.descuentos.umbral import pct_por_importe
def total(base):
precio = con_iva(base)
return aplica(precio, pct_por_importe(precio))
programa.py
from tienda.ticket import total
print("Compra de 50 €:", total(50), "€")
print("Compra de 100 €:", total(100), "€")
Por qué está hecho así:
- Los dos
__init__.py(el detienday el dedescuentos) están vacíos, y son los que dicen que esas carpetas son paquetes. ticket.pyusa tres importaciones con ruta completa desdetienda. Es el módulo que junta las demás piezas, y por eso el único que importa de varios sitios.- La decisión del descuento se toma con el precio con IVA:
pct_por_importe(precio). Si la tomaras con la base, una compra de 90 € de base (108,90 € con IVA) se quedaría sin descuento aunque el ticket supere los 100 €. 10 if importe >= 100 else 0es una expresión condicional en una sola línea; equivale a unifnormal con dosreturn.- Los números cuadran: 50 € → 60,50 € (sin descuento); 100 € → 121 € → 10 % de descuento → 108,90 €.
programa.pyestá fuera detienday lo lanzas desde esa carpeta; si lo metieras dentro, Python dejaría de ver el paquetetienda.
Míralo paso a paso en Python Tutor
El fallo del fragmento 2 es perfecto para verlo en vivo, aunque Python Tutor solo admite un archivo. Elige Python 3 y pega este programa, que hace en una sola página lo mismo que los dos from … import aplica:
def aplica_iva(precio):
return round(precio * 1.21, 2)
def aplica_descuento(precio):
return round(precio * 0.90, 2)
aplica = aplica_iva # como: from impuestos import aplica
print("Con IVA:", aplica(100))
aplica = aplica_descuento # como: from descuentos import aplica
print("Con IVA:", aplica(100)) # ¡el nombre ya apunta a la otra función!
Salida esperada:
Con IVA: 121.0 Con IVA: 90.0
Qué observar, paso a paso:
- Tras las dos definiciones, en las variables globales hay dos funciones:
aplica_ivayaplica_descuento. Todavía no existe ninguna llamadaaplica. - En
aplica = aplica_ivaaparece el nombreaplicacon una flecha hacia la misma caja queaplica_iva. Esto equivale afrom impuestos import aplica. La llamada que sigue imprime121.0. - En
aplica = aplica_descuento, la flecha deaplicasalta a la otra caja. Esto equivale al segundofrom … import. Y la caja deaplica_ivasigue existiendo, pero ya no está apuntada poraplica. - La segunda llamada,
aplica(100), ahora ejecuta la función del descuento: por eso imprime90.0aunque el mensaje diga «con IVA». - Si en lugar de reasignar dejaras dos nombres distintos (
aplica_ivayaplica_descuento, o los prefijosimpuestos.ydescuentos.), cada llamada iría a su función.
Chuletario: Módulos y paquetes
Lo esencial en 30 segundos
- Módulo = un archivo
.py. El nombre del módulo es el del archivo, sin la extensión. - **
import m** →m.f(). **from m import f** →f(). **as** da un alias. - Importar ejecuta el archivo (la primera vez). Deja solo definiciones al nivel superior.
- **
if __name__ == "__main__":** → código que solo corre al lanzar el archivo directamente. - Paquete = carpeta con módulos y
__init__.py. Un punto en elimportes una carpeta. - **Dos
fromcon el mismo nombre**: el último gana, sin error.
Sintaxis
| Quiero… | Código | Resultado |
|---|---|---|
| Usar un módulo con prefijo | import notas | notas.media(...) |
| Traer una función suelta | from notas import media | media(...) |
| Abreviar un módulo | import notas as nt | nt.media(...) |
| Cambiar el nombre de una función | from notas import media as media_notas | media_notas(...) |
| Código que solo corre lanzando el archivo | if __name__ == "__main__": | Se salta al importar |
| Un módulo de un paquete | import tienda.precios | tienda.precios.con_iva(...) |
| Una función de un subpaquete | from tienda.descuentos.umbral import pct_por_importe | pct_por_importe(...) |
| Un módulo sin ruta completa | from tienda.descuentos import umbral | umbral.pct_por_importe(...) |
| Convertir una carpeta en paquete | Archivo __init__.py (puede estar vacío) | Python la trata como paquete |
Plantillas de los ejercicios
Un módulo que se puede importar y lanzar (Nivel 2):
def formatea(cantidad):
return f"{cantidad:.2f} €"
if __name__ == "__main__": # solo si lanzas este archivo
print(formatea(5))
Dos módulos con el mismo nombre de función (Nivel 2):
from euros import formatea as en_euros from dolares import formatea as en_dolares
Un módulo que usa otro del mismo paquete (Nivel 3):
from tienda.precios import con_iva # desde la carpeta de arriba from tienda.descuentos.porcentaje import aplica
Errores que más se repiten
| Síntoma | Causa probable |
|---|---|
| Salen líneas que no has pedido al importar | Código suelto al nivel superior del módulo: ponlo bajo if __name__ == "__main__": |
NameError: name 'media' is not defined | Hiciste import notas y llamaste a media sin el prefijo notas. |
ModuleNotFoundError: No module named 'x' | Nombre o ruta mal escritos, o ejecutas desde la carpeta equivocada |
ModuleNotFoundError al lanzar un archivo de dentro del paquete | Python solo ve el paquete desde la carpeta de arriba: lanza programa.py, no los módulos internos |
AttributeError: module 'tienda' has no attribute 'precios' | import tienda no importa sus módulos: usa import tienda.precios |
| Un resultado distinto del esperado, sin error | Dos from … import con el mismo nombre: el segundo pisó al primero |
TypeError: f() takes 2 positional arguments but 3 were given | Llamas a una función con el nombre de otra que pisó a la tuya |
Errores raros al importar math, random… | Tu archivo se llama igual que un módulo estándar |
Antes de entregar, pregúntate
- ¿Mi módulo solo define cosas al nivel superior, con las pruebas bajo
if __name__ == "__main__":? - ¿He evitado traer con
fromnombres repetidos de dos módulos (o les he puesto alias)? - ¿Cada carpeta de mi paquete tiene su **
__init__.py**, y mis rutas deimportparten de la carpeta de arriba? - ¿Lanzo el programa desde fuera del paquete, no un módulo de su interior?
Si quieres repasar el tema desde la base, vuelve a la teoría de módulos y paquetes.
Sigue aprendiendoTodos los artículos de Python¿Te ha servido este artículo? Así puedes apoyar SergioLearns →
