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.

Cómo usar este artículo

  1. Lee el enunciado de un nivel y no mires la solución todavía.
  2. Escribe tu versión, ejecútala y compara tu salida con la esperada.
  3. Si te atascas, mira solo la 🔵 pista y vuelve a intentarlo.
  4. 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 constante IVA = 0.21 y con_iva(base), que devuelve el precio con IVA redondeado a dos decimales.
  • descuentos/porcentaje.py: aplica(precio, pct), que rebaja el precio un pct por ciento (redondeado a dos decimales).
  • descuentos/umbral.py: pct_por_importe(importe), que devuelve 10 si el importe es 100 o más, y 0 si 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 temp le da un alias corto al módulo; con él llamamos a temp.c_a_f(20) y leemos temp.CERO_ABSOLUTO_C. Todo sigue con prefijo.
  • from temperatura import c_a_k trae solo esa función, que usamos sin prefijo. En el mismo programa conviven las dos formas, y no se estorban.
  • c - CERO_ABSOLUTO_C suma 273,15 porque la constante es negativa. Si hubieras escrito c + 273.15 a 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 formatea y no hay conflicto porque viven en módulos distintos. El conflicto solo aparecería al traerlas con from sin alias, y por eso usamos as.
  • en_euros y en_dolares son etiquetas para dos funciones diferentes: dicen en el propio programa qué hace cada una, algo que formatea a secas no decía.
  • Además de los alias, programa.py hace import euros para 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 con python3 euros.py (segunda salida). Al importar euros desde programa.py, __name__ vale "euros" y el bloque se salta (primera salida).
  • :.2f fuerza 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 de tienda y el de descuentos) están vacíos, y son los que dicen que esas carpetas son paquetes.
  • ticket.py usa tres importaciones con ruta completa desde tienda. 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 0 es una expresión condicional en una sola línea; equivale a un if normal con dos return.
  • Los números cuadran: 50 € → 60,50 € (sin descuento); 100 € → 121 € → 10 % de descuento → 108,90 €.
  • programa.py está fuera de tienda y lo lanzas desde esa carpeta; si lo metieras dentro, Python dejaría de ver el paquete tienda.

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:

  1. Tras las dos definiciones, en las variables globales hay dos funciones: aplica_iva y aplica_descuento. Todavía no existe ninguna llamada aplica.
  2. En aplica = aplica_iva aparece el nombre aplica con una flecha hacia la misma caja que aplica_iva. Esto equivale a from impuestos import aplica. La llamada que sigue imprime 121.0.
  3. En aplica = aplica_descuento, la flecha de aplica salta a la otra caja. Esto equivale al segundo from … import. Y la caja de aplica_iva sigue existiendo, pero ya no está apuntada por aplica.
  4. La segunda llamada, aplica(100), ahora ejecuta la función del descuento: por eso imprime 90.0 aunque el mensaje diga «con IVA».
  5. Si en lugar de reasignar dejaras dos nombres distintos (aplica_iva y aplica_descuento, o los prefijos impuestos. y descuentos.), 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 el import es una carpeta.
  • **Dos from con el mismo nombre**: el último gana, sin error.

Sintaxis

Quiero…CódigoResultado
Usar un módulo con prefijoimport notasnotas.media(...)
Traer una función sueltafrom notas import mediamedia(...)
Abreviar un móduloimport notas as ntnt.media(...)
Cambiar el nombre de una funciónfrom notas import media as media_notasmedia_notas(...)
Código que solo corre lanzando el archivoif __name__ == "__main__":Se salta al importar
Un módulo de un paqueteimport tienda.preciostienda.precios.con_iva(...)
Una función de un subpaquetefrom tienda.descuentos.umbral import pct_por_importepct_por_importe(...)
Un módulo sin ruta completafrom tienda.descuentos import umbralumbral.pct_por_importe(...)
Convertir una carpeta en paqueteArchivo __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íntomaCausa probable
Salen líneas que no has pedido al importarCódigo suelto al nivel superior del módulo: ponlo bajo if __name__ == "__main__":
NameError: name 'media' is not definedHiciste 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 paquetePython 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 errorDos from … import con el mismo nombre: el segundo pisó al primero
TypeError: f() takes 2 positional arguments but 3 were givenLlamas 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

  1. ¿Mi módulo solo define cosas al nivel superior, con las pruebas bajo if __name__ == "__main__":?
  2. ¿He evitado traer con from nombres repetidos de dos módulos (o les he puesto alias)?
  3. ¿Cada carpeta de mi paquete tiene su **__init__.py**, y mis rutas de import parten de la carpeta de arriba?
  4. ¿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 →

Sigue aprendiendo

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *