TareaFácil funciona: la v0.16 tiene clases con validación, una agenda que protege sus datos y persistencia en JSON. Pero todo eso vive en un único fichero que ya acumula tres clases, una docena de funciones de entrada y salida, las constantes, el menú y la persistencia. Cada vez que hay que tocar algo toca recorrerlo entero buscando dónde, y si mañana Marta quiere una versión web, no hay forma de reutilizar la lógica sin arrastrar también el menú de la consola. Esta lección resuelve ese problema con la última pieza de organización que le falta al curso: repartir el código en varios ficheros que se importan entre sí. Verás qué es un módulo y qué ocurre exactamente cuando Python importa uno —lo que explica por fin el if __name__ == "__main__": que arrastramos desde 04-04—, cómo se agrupan varios módulos en un paquete, qué tesoros trae la biblioteca estándar y cómo se instalan librerías de terceros con pip. Al final, TareaFácil dejará de ser un fichero para convertirse en el paquete tareafacil/.

Contenido

  1. Qué es un módulo y por qué separar
  2. Las formas de importar
  3. Cómo encuentra Python los módulos
  4. Qué ocurre al importar: __name__ y if __name__ == "__main__"
  5. Paquetes: carpetas con __init__.py
  6. La biblioteca estándar
  7. Paquetes de terceros: PyPI y pip
  8. Importaciones circulares
  9. TareaFácil v0.17: el paquete tareafacil/
  10. Errores comunes y consejos
  11. Ejercicios
  12. Conclusión

  1. Qué es un módulo y por qué separar

Un módulo es, simplemente, un fichero .py. No hay que declarar nada ni marcarlo de ninguna manera: en cuanto guardas validaciones.py, ya tienes un módulo llamado validaciones que cualquier otro fichero puede importar. Lo llevas usando desde el principio del curso sin saberlo: import json, import csv, from pathlib import Path cargan módulos escritos por otros.

¿Por qué repartir el código en varios ficheros? Por cuatro razones muy concretas:

  • Encontrar las cosas. En un proyecto con modelo.py, agenda.py, almacen.py e interfaz.py, saber dónde tocar es inmediato. En un fichero de mil líneas, no.
  • Reutilizar. Si la lógica de tareas vive en modelo.py sin una sola llamada a print ni a input, ese fichero sirve igual para la aplicación de consola, para una web o para un script de informes.
  • Trabajar en equipo. Dos personas en dos ficheros distintos no se pisan; en el mismo fichero, sí.
  • Limitar el daño. Un módulo con una responsabilidad clara se puede leer, entender y cambiar sin tener toda la aplicación en la cabeza.

El criterio para decidir qué va en cada fichero es el mismo que ya usaste con las funciones en 04-04 y con las clases en este módulo: una responsabilidad por módulo. Los datos por un lado, la persistencia por otro, la conversación con el usuario por otro.

  1. Las formas de importar

Supongamos un fichero formato.py con una constante y una función:

# formato.py
ANCHO = 52

def titulo(texto):
    """Devuelve el texto centrado y en mayusculas."""
    return texto.upper().center(ANCHO)

Desde otro fichero, en la misma carpeta, se puede acceder de cuatro formas:

Forma Cómo se usa después Cuándo conviene
import formato formato.titulo("hola") La más segura: queda claro de dónde sale cada nombre
import formato as fmt fmt.titulo("hola") Cuando el nombre del módulo es largo (import numpy as np)
from formato import titulo titulo("hola") Cuando usas uno o dos nombres muchas veces
from formato import titulo as tit tit("hola") Para evitar un choque de nombres
from formato import * titulo("hola") Desaconsejado
import formato
print(formato.titulo("agenda de hoy"))       # el prefijo dice de donde viene
from formato import titulo, ANCHO
print(titulo("agenda de hoy"), ANCHO)        # sin prefijo, mas corto

La última fila de la tabla merece explicación. from modulo import * trae todos los nombres públicos del módulo al fichero actual, y está desaconsejado por dos motivos serios: quien lea tu código no puede saber de dónde sale titulo —¿lo has definido tú, viene de formato o de otro import *?—, y si dos módulos exportan el mismo nombre, el segundo pisa al primero sin decir nada. Un from os import * seguido de from numpy import * puede dejarte con una función distinta de la que crees estar llamando. Es un ahorro de tecleo que se paga con horas de depuración. La regla práctica que siguen casi todos los proyectos: import modulo por defecto, from modulo import nombre cuando se usa mucho un nombre concreto, y nunca *.

  1. Cómo encuentra Python los módulos

Cuando escribes import formato, Python busca ese fichero en una lista de carpetas, en orden:

  1. La carpeta del script que has ejecutado (o el directorio actual, en la consola interactiva).
  2. Las carpetas indicadas en la variable de entorno PYTHONPATH, si existe.
  3. Las carpetas de la instalación de Python, donde está la biblioteca estándar.
  4. La carpeta site-packages, donde pip instala los paquetes de terceros.

Esa lista es visible y se puede consultar:

import sys
for carpeta in sys.path:
    print(carpeta)

Si Python no encuentra el módulo, el error es inconfundible: ModuleNotFoundError: No module named 'formato'. Las causas habituales son tres: el fichero no está en la carpeta que crees, el nombre está mal escrito (mayúsculas incluidas: en Linux Formato y formato son distintos), o has ejecutado el script desde otra carpeta.

Y hay una trampa que hay que conocer sí o sí: nunca llames a tus ficheros como un módulo de la biblioteca estándar. Si creas un random.py para practicar y dentro escribes import random, Python encontrará tu fichero antes que el oficial —la carpeta del script va primero— y fallará con un error incomprensible del tipo AttributeError: module 'random' has no attribute 'randint'. Lo mismo con json.py, csv.py, math.py, time.py o email.py. Si te ocurre, renombra tu fichero y borra la carpeta __pycache__ que Python haya dejado al lado.

  1. Qué ocurre al importar: __name__ y if __name__ == "__main__"

Aquí llega la explicación que prometimos en 04-04. Cuando Python importa un módulo, ejecuta su código de arriba abajo, entero, una sola vez. Las líneas def y class definen funciones y clases sin ejecutarlas, pero cualquier otra línea sí se ejecuta: asignaciones, print, llamadas.

Y en esa ejecución, Python define en el módulo una variable especial llamada __name__, con un valor que depende de cómo se haya llegado a él:

Situación Valor de __name__
El fichero se ejecuta directamente (python programa.py) "__main__"
El fichero se importa desde otro (import programa) "programa" (su nombre de módulo)

Vamos a comprobarlo con dos ficheros:

# saludo.py
print(f"Ejecutando saludo.py, __name__ vale: {__name__}")

def saludar(nombre):
    return f"Hola, {nombre}."

if __name__ == "__main__":
    print(saludar("Marta"))          # solo si ejecutamos ESTE fichero
# principal.py
import saludo
print(f"Ejecutando principal.py, __name__ vale: {__name__}")
print(saludo.saludar("Luis"))

Al ejecutar python saludo.py salen dos líneas: Ejecutando saludo.py, __name__ vale: __main__ y Hola, Marta.. Pero al ejecutar python principal.py la salida es esta:

Ejecutando saludo.py, __name__ vale: saludo
Ejecutando principal.py, __name__ vale: __main__
Hola, Luis.

Léela con calma, porque contiene tres lecciones:

  • El print de saludo.py se ejecutó aunque solo lo hayamos importado: importar es ejecutar. Por eso un módulo no debe hacer trabajo pesado ni pedir datos al usuario en su nivel superior.
  • __name__ valió "saludo", no "__main__", así que la línea print(saludar("Marta")) no se ejecutó. Ese es exactamente el propósito del if: el código que solo tiene sentido al ejecutar el fichero directamente.
  • __name__ vale "__main__" en el fichero que arrancó todo, sea cual sea su nombre.

De ahí sale la conclusión que llevábamos arrastrando: if __name__ == "__main__": main() significa «arranca la aplicación solo si me están ejecutando a mí; si alguien me importa para reutilizar mis funciones, no hagas nada». Sin esa guarda, importar tareafacil para usar su clase Tarea lanzaría el menú interactivo por sorpresa.

Un último detalle: al importar por primera vez, Python guarda una versión compilada en __pycache__ para acelerar las siguientes ejecuciones, y no vuelve a ejecutar el módulo si ya está importado, aunque aparezcan diez import iguales.

  1. Paquetes: carpetas con __init__.py

Cuando los módulos se multiplican, se agrupan en carpetas. Una carpeta que Python trata como un conjunto de módulos es un paquete, y para marcarla como tal se le añade un fichero llamado __init__.py.

graph TD
    A["proyecto/"] --> B["main.py"]
    A --> C["tareafacil/"]
    C --> D["__init__.py"]
    C --> E["modelo.py"]
    C --> F["agenda.py"]
    C --> G["almacen.py"]
    C --> H["interfaz.py"]

Con esa estructura, los módulos se importan con la notación del punto: import tareafacil.modelo para el módulo completo, from tareafacil.modelo import Tarea para un nombre concreto o from tareafacil import agenda para un módulo del paquete. El __init__.py se ejecuta al importar cualquier cosa del paquete, y sirve para dos cosas: dejarlo vacío (lo más habitual y perfectamente correcto), o usarlo para exponer una interfaz cómoda, de modo que quien use el paquete no tenga que conocer su estructura interna:

# tareafacil/__init__.py
"""Paquete TareaFacil: gestor de tareas del Estudio Alba."""
from .modelo import Tarea, TareaRecurrente
from .agenda import Agenda

__version__ = "0.17"

Gracias a eso, quien use el paquete puede escribir from tareafacil import Tarea, Agenda sin saber en qué fichero está cada clase. Lo que no debe hacer nunca un __init__.py es trabajo pesado, porque se ejecuta en cada importación. Y fíjate en el punto de from .modelo import Tarea: es una importación relativa, y significa «el módulo modelo que está en mi mismo paquete».

Tipo Ejemplo Cuándo usarla
Absoluta from tareafacil.modelo import Tarea Por defecto: se lee sin ambigüedad desde cualquier sitio
Relativa from .modelo import Tarea Dentro del propio paquete; .. sube un nivel

Las dos funcionan y las verás en proyectos reales. La guía de estilo oficial recomienda las absolutas por claridad, con las relativas como opción aceptable dentro de un paquete grande. Lo que no conviene es mezclar los dos estilos sin criterio en el mismo proyecto.

  1. La biblioteca estándar

Python viene «con las pilas puestas»: al instalarlo tienes cientos de módulos ya disponibles, sin instalar nada. Estos son los que más te van a servir ahora:

Módulo Para qué sirve
math Raíces, potencias, redondeos precisos, constantes como pi
random Números aleatorios, choice, shuffle, sample
datetime Fechas y horas: cálculos, diferencias y formatos
os Sistema operativo: variables de entorno, procesos, rutas antiguas
pathlib Rutas modernas orientadas a objetos: Path, exists(), read_text()
json Leer y escribir JSON (05-05)
csv Leer y escribir CSV, con DictReader y DictWriter (05-05)
sys Intérprete: sys.path, sys.argv, sys.exit()
time Pausas con sleep y medición con perf_counter (06-02)
statistics mean, median, stdev sin instalar nada
collections Counter, defaultdict, namedtuple, deque
functools lru_cache (06-03), partial, reduce

Un ejemplo con datetime aplicado a nuestras tareas, que es el uso que más falta le hace a TareaFácil: pasar de «días estimados» a fechas de entrega reales.

from datetime import date, timedelta

hoy = date.today()                               # date(2026, 8, 5)
entrega = hoy + timedelta(days=3)                # sumar dias es asi de simple
print(entrega.strftime("%d/%m/%Y"))              # 08/08/2026

quedan = (entrega - hoy).days                    # restar fechas da un timedelta
print(f"Quedan {quedan} dias.")                  # Quedan 3 dias.
print(entrega < hoy)                             # False: las fechas se comparan

Tres piezas: date.today() da la fecha actual, timedelta(days=n) representa una duración que se suma o resta a una fecha, y strftime la formatea (%d día, %m mes, %Y año). Restar dos fechas devuelve un timedelta, cuyo .days da la diferencia. Y como las fechas se comparan con < y >, ordenar tareas por fecha de entrega con sorted(..., key=attrgetter("entrega")) funciona sin más.

  1. Paquetes de terceros: PyPI y pip

Cuando la biblioteca estándar no llega, se recurre a PyPI (Python Package Index), el repositorio público donde la comunidad publica más de medio millón de paquetes: requests para hablar con webs, pandas para analizar datos, flask y django para aplicaciones web, pytest para pruebas.

Se instalan con pip, el gestor de paquetes que viene con Python:

pip install requests                 # instalar
pip install requests==2.31.0         # una version concreta
pip list                             # ver lo instalado
pip freeze > requirements.txt        # guardar la lista exacta de versiones
pip install -r requirements.txt      # reinstalarla en otro ordenador
pip uninstall requests               # desinstalar

El fichero requirements.txt es la pieza clave del trabajo en equipo: contiene las dependencias con su versión exacta, se guarda junto al código y permite que cualquiera reproduzca tu entorno con un solo comando.

Y aquí conviene recordar el entorno virtual de Entornos de desarrollo. Los paquetes deben instalarse dentro del venv del proyecto, no en el Python del sistema, por dos motivos: dos proyectos pueden necesitar versiones distintas de la misma librería, y así el requirements.txt refleja exactamente lo que ese proyecto usa y nada más.

Una advertencia de seguridad que ya nunca deberías olvidar: instalar un paquete es ejecutar código de un desconocido en tu ordenador. Antes de un pip install, comprueba que el nombre está bien escrito —existen paquetes maliciosos con nombres casi idénticos a los populares, a la caza de erratas—, mira si el proyecto tiene actividad reciente y usuarios, y desconfía de dependencias que aparecen sin que sepas quién las pidió. TareaFácil, por cierto, no necesita ninguna: todo lo que usa está en la biblioteca estándar.

  1. Importaciones circulares

Ocurre cuando dos módulos se importan mutuamente: agenda.py hace import almacen y almacen.py hace import agenda. Python empieza a cargar el primero, ve el import del segundo, empieza a cargar el segundo, que vuelve al primero... que todavía está a medio ejecutar, y por tanto le faltan nombres. El resultado es un ImportError: cannot import name 'X' from partially initialized module que desconcierta a cualquiera.

La solución no es técnica sino de diseño: las dependencias deben ir en una sola dirección. En TareaFácil, almacen sabe de modelo, e interfaz sabe de las dos, pero modelo no sabe nada de nadie. Si te encuentras con un ciclo, casi siempre significa que:

  • Las responsabilidades están mal repartidas y hay algo que debería estar en un tercer módulo del que dependan los dos.
  • O una de las dos direcciones sobra: normalmente el módulo «de abajo» no necesita conocer al de arriba, sino recibir lo que necesite como parámetro.

  1. TareaFácil v0.17: el paquete tareafacil/

Repartimos la v0.16 en cinco módulos, cada uno con una responsabilidad:

graph TD
    A["tareafacil/"] --> B["__init__.py"]
    A --> C["modelo.py<br/>Tarea, TareaRecurrente"]
    A --> D["agenda.py<br/>Agenda"]
    A --> E["almacen.py<br/>JSON y CSV"]
    A --> F["interfaz.py<br/>menu y entrada/salida"]
    A --> G["__main__.py<br/>main()"]

Y estas son las cabeceras de cada uno, que muestran quién depende de quién:

# tareafacil/modelo.py --- no importa nada del proyecto: es la base
PRIORIDADES = ("alta", "media", "baja")
EQUIPO = ("marta", "luis", "nuria")
ORDEN_PRIORIDAD = {"alta": 0, "media": 1, "baja": 2}

class Tarea: ...
class TareaRecurrente(Tarea): ...

# tareafacil/agenda.py
from .modelo import Tarea, ORDEN_PRIORIDAD

class Agenda: ...                       # sin guardar() ni cargar(): eso es del almacen
# tareafacil/almacen.py
import csv, json
from pathlib import Path
from .modelo import Tarea
from .agenda import Agenda

RUTA_JSON = Path("tareas.json")
RUTA_CSV = Path("tareas.csv")
CAMPOS = ("titulo", "responsable", "prioridad", "dias", "hechos", "completada")

def guardar(agenda, ruta=RUTA_JSON): ...
def cargar(ruta=RUTA_JSON): ...         # devuelve una Agenda
def exportar_csv(agenda, ruta=RUTA_CSV): ...
# tareafacil/interfaz.py
from .modelo import PRIORIDADES, EQUIPO, Tarea
from .agenda import Agenda

ANCHO = 52
OPCIONES = ("1", "2", "3", "4", "5", "6", "7", "8", "9", "10")

def pedir_texto(mensaje): ...           # y pedir_opcion, pedir_entero, confirmar
def mostrar_menu(): ...                 # y mostrar_listado, mostrar_ficha
# tareafacil/__main__.py
from . import almacen, interfaz
from .modelo import Tarea

def main():
    """Ejecuta el bucle principal de TareaFacil."""
    agenda = almacen.cargar()
    while True:
        interfaz.mostrar_menu()
        opcion = interfaz.pedir_opcion("Elige una opcion (1-10): ", interfaz.OPCIONES)
        if opcion == "10" and interfaz.confirmar("Seguro que quieres salir?"):
            almacen.guardar(agenda)
            break
        # ... el resto de opciones

if __name__ == "__main__":
    main()

Y así se ejecuta ahora el programa, desde la carpeta que contiene a tareafacil/:

python -m tareafacil

La opción -m le dice a Python «ejecuta este paquete como programa», y por eso el fichero se llama __main__.py: es el que Python busca para arrancar un paquete. La alternativa clásica es dejar fuera de la carpeta un main.py de tres líneas —from tareafacil.__main__ import main, y la guarda if __name__ == "__main__": main()— y ejecutar python main.py. Fíjate en el resultado, que es lo importante de toda la lección:

Módulo Responsabilidad ¿De quién depende?
modelo.py Qué es una tarea y qué sabe hacer De nadie
agenda.py La colección y sus operaciones De modelo
almacen.py Leer y escribir ficheros De modelo y agenda
interfaz.py Hablar con el usuario De modelo y agenda
__main__.py Coordinar el flujo De todos

Las flechas van en una sola dirección, así que no hay importaciones circulares. Y modelo.py, que no depende de nada y no tiene un solo print, se puede reutilizar tal cual en una versión web o probar de forma aislada, que es justo lo que haremos en 08-04.

Errores Comunes y Consejos

  • Llamar a un fichero como un módulo estándar (random.py, json.py, csv.py). Python cargará el tuyo y el error será incomprensible. Renombra y borra __pycache__.
  • Usar from modulo import *. Destruye la trazabilidad de los nombres y provoca choques silenciosos.
  • Ejecutar el paquete desde dentro de su carpeta. python __main__.py desde dentro de tareafacil/ rompe las importaciones relativas (attempted relative import with no known parent package). Sitúate en la carpeta de arriba y usa python -m tareafacil.
  • Poner código ejecutable en el nivel superior de un módulo. Recuerda que importar es ejecutar: un input() o un print sueltos se dispararán en cuanto alguien importe el fichero. Todo lo ejecutable, dentro de funciones y bajo if __name__ == "__main__":.
  • Crear ciclos entre módulos. Si a importa b y b importa a, revisa el reparto de responsabilidades: casi siempre sobra una de las dos direcciones.
  • Instalar paquetes fuera del entorno virtual. Acabas con un Python del sistema lleno de librerías y un requirements.txt que no refleja el proyecto.
  • Consejo: empieza por un fichero y separa cuando duela. Dividir demasiado pronto crea diez ficheros de veinte líneas y un lío de importaciones. La señal para separar es concreta: cuando cueste encontrar dónde tocar o cuando quieras reutilizar una parte sin las demás.

Ejercicios

Ejercicio 1: Módulo, importación y __name__

Crea medidas.py con una constante IVA = 0.21, una función con_iva(base) que devuelva el importe con IVA y un print("cargando medidas") en el nivel superior. Crea después factura.py que lo importe y calcule el total de un presupuesto de 1.200 € para el cliente Vidal. Predice qué imprimirá cada uno al ejecutarse directamente y compruébalo; después añade a medidas.py una prueba rápida protegida con if __name__ == "__main__": y explica qué cambia.

Ejercicio 2: Repartir responsabilidades

Un compañero ha escrito un único fichero gestor.py de 600 líneas con: la clase Cliente, la clase Factura, funciones para leer y escribir el CSV de facturas, funciones que piden datos por teclado, el menú y el main(). Propón un reparto en módulos, indica qué importa cada uno y dibuja mentalmente las flechas de dependencia para comprobar que no hay ciclos.

Ejercicio 3: Fechas de entrega con datetime

Escribe una función fecha_entrega(dias) que devuelva la fecha de entrega a partir de hoy formateada como dd/mm/aaaa, y otra dias_para(fecha) que reciba un date y devuelva cuántos días faltan (negativo si ya pasó). Úsalas para imprimir el plan de las tres tareas de Estudio Alba: cartel (3 días), logotipo (5) y presupuesto (2).

Soluciones

Solución 1.

# medidas.py
print("cargando medidas")
IVA = 0.21

def con_iva(base):
    """Devuelve el importe con el IVA aplicado."""
    return round(base * (1 + IVA), 2)

if __name__ == "__main__":
    print(con_iva(100))          # 121.0: prueba rapida, solo al ejecutar este fichero

# factura.py
import medidas
print(f"Presupuesto cliente Vidal: {medidas.con_iva(1200)} EUR")

Al ejecutar python factura.py la salida es cargando medidas y después Presupuesto cliente Vidal: 1452.0 EUR. El primer mensaje aparece aunque no lo hayamos pedido, porque importar es ejecutar; el 121.0 de la prueba no aparece, porque al importarse __name__ vale "medidas" y no "__main__". Esa guarda es lo que permite que un módulo tenga su propia comprobación rápida sin molestar a quien lo reutiliza. Y el print("cargando medidas") suelto es justo lo que no debe hacer un módulo de verdad: aquí está solo para verlo.

Solución 2.

Módulo Contenido Importa
modelo.py Clases Cliente y Factura Nada del proyecto
almacen.py Leer y escribir el CSV de facturas modelo
interfaz.py Pedir datos por teclado y mostrar el menú modelo
principal.py El main() y el bucle de opciones Los tres anteriores

Las flechas van de principal hacia los demás y de almacen/interfaz hacia modelo: ninguna vuelve hacia atrás, así que no hay ciclos. La clave del reparto es que modelo.py no importe nada del proyecto ni contenga un solo print o input; si Factura necesitara pedir algo al usuario, sería señal de que esa lógica está en el sitio equivocado. Si las cuatro piezas crecen, el paso siguiente es agruparlas en un paquete gestor/ con su __init__.py.

Solución 3.

from datetime import date, timedelta

def fecha_entrega(dias):
    """Devuelve la fecha de entrega dentro de 'dias', como dd/mm/aaaa."""
    return (date.today() + timedelta(days=dias)).strftime("%d/%m/%Y")

def dias_para(fecha):
    """Dias que faltan para una fecha; negativo si ya ha pasado."""
    return (fecha - date.today()).days

for titulo, dias in (("Cartel feria del libro", 3), ("Logotipo Sole", 5),
                     ("Presupuesto cliente Vidal", 2)):
    print(f"{titulo:<28} entrega el {fecha_entrega(dias)}")
print(dias_para(date(2026, 1, 1)))       # negativo: esa fecha ya paso

Dos detalles que conviene fijar. date.today() + timedelta(days=dias) devuelve un objeto date nuevo: las fechas son inmutables, como las tuplas y las cadenas, así que nunca se modifican en el sitio. Y strftime se aplica solo al final, para mostrar: mientras el programa calcula, conviene trabajar siempre con objetos date y no con textos, porque los objetos se comparan, se restan y se ordenan, y "08/08/2026" no.

Conclusión

Un módulo es cualquier fichero .py, y separar el código en varios responde a cuatro necesidades muy concretas: encontrar las cosas, reutilizar la lógica sin arrastrar la interfaz, trabajar en equipo y limitar el daño de cada cambio. Se importan con import modulo —la forma más clara, porque el prefijo dice de dónde sale cada nombre—, import modulo as alias, from modulo import nombre y su variante con as, evitando siempre from modulo import *, que borra la trazabilidad y provoca choques silenciosos. Python busca los módulos en la carpeta del script, en PYTHONPATH, en la instalación y en site-packages, lista visible en sys.path; de ahí el ModuleNotFoundError y la trampa de llamar a un fichero propio random.py o json.py. Al importar, el módulo se ejecuta entero una sola vez, y Python le da a __name__ el valor "__main__" solo si es el fichero que has lanzado: esa es la explicación completa del if __name__ == "__main__": que veníamos usando desde 04-04, la guarda que separa «lo que hago cuando me ejecutan» de «lo que ofrezco cuando me importan». Varios módulos en una carpeta con __init__.py forman un paquete, con importaciones absolutas (from tareafacil.modelo import Tarea) o relativas (from .modelo import Tarea), y con un __init__.py que puede quedarse vacío o exponer una interfaz cómoda. La biblioteca estándar trae ya resuelto casi todo —math, random, datetime, os, pathlib, json, csv, sys, time, statistics, collections, functools—, y para lo demás está PyPI con pip install y requirements.txt, siempre dentro del venv del proyecto y siempre mirando qué se instala. Por último, las importaciones circulares no se arreglan con trucos: se evitan haciendo que las dependencias vayan en una sola dirección.

Con esto se cierra el módulo 7 y el salto más grande del curso. Empezaste con una lista de diccionarios frágil, en la que cualquier tarea podía nacer sin completada o con la prioridad escrita como "Alta", y con las funciones que operaban sobre ellas desperdigadas por un fichero. Ahora hay una clase Tarea que garantiza en su constructor que ninguna tarea inválida llega a existir y que lleva dentro su propio comportamiento; una clase Agenda que encapsula la colección, valida lo que entra, protege su lista interna y sabe guardarse y reconstruirse desde JSON; herencia y polimorfismo en su forma justa, con la advertencia de preferir composición; y un proyecto repartido en el paquete tareafacil/, con cada módulo en su sitio y las dependencias apuntando en una sola dirección. TareaFácil es la v0.17 y se ejecuta con python -m tareafacil.

Y sin embargo, míralo con ojos de programador profesional: ese código no está documentado más allá de unas docstrings sueltas, y nadie de fuera sabría por dónde entrar; no gestiona los errores, así que un fichero JSON corrupto o un dias que llega como texto todavía tumban el programa entero con un traceback; no tiene historial, de modo que un cambio desafortunado se pierde sin vuelta atrás y no hay forma de saber quién tocó qué; y no tiene pruebas, así que cada modificación obliga a repasar el menú a mano rezando por no haber roto nada. En el módulo 8 se resuelven las cuatro cosas, empezando por Documentación y comentarios: docstrings de verdad, comentarios que explican el porqué y no el qué, y un README que permita a cualquiera —incluido tú dentro de seis meses— entender el proyecto en cinco minutos.

Fundamentos de la Programación

Módulo 1: Introducción a la Programación

Módulo 2: Conceptos Básicos

Módulo 3: Estructuras de Control

Módulo 4: Funciones y Procedimientos

Módulo 5: Estructuras de Datos

Módulo 6: Algoritmos Básicos

Módulo 7: Objetos y Organización del Código

Módulo 8: Buenas Prácticas y Herramientas

Módulo 9: Proyecto Final y Cierre del Curso

© Copyright 2026. Todos los derechos reservados