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
- Qué es un módulo y por qué separar
- Las formas de importar
- Cómo encuentra Python los módulos
- Qué ocurre al importar:
__name__yif __name__ == "__main__" - Paquetes: carpetas con
__init__.py - La biblioteca estándar
- Paquetes de terceros: PyPI y
pip - Importaciones circulares
- TareaFácil v0.17: el paquete
tareafacil/ - Errores comunes y consejos
- Ejercicios
- Conclusión
- 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.pyeinterfaz.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.pysin una sola llamada aprintni ainput, 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.
- 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 cortoLa ú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 *.
- Cómo encuentra Python los módulos
Cuando escribes import formato, Python busca ese fichero en una lista de carpetas, en orden:
- La carpeta del script que has ejecutado (o el directorio actual, en la consola interactiva).
- Las carpetas indicadas en la variable de entorno
PYTHONPATH, si existe. - Las carpetas de la instalación de Python, donde está la biblioteca estándar.
- La carpeta
site-packages, dondepipinstala los paquetes de terceros.
Esa lista es visible y se puede consultar:
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.
- Qué ocurre al importar:
__name__ y if __name__ == "__main__"
__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
printdesaludo.pyse 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íneaprint(saludar("Marta"))no se ejecutó. Ese es exactamente el propósito delif: 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.
- Paquetes: carpetas con
__init__.py
__init__.pyCuando 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.
- 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 comparanTres 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.
- Paquetes de terceros: PyPI y
pip
pipCuando 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.
- 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.
- TareaFácil v0.17: el paquete
tareafacil/
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/:
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__.pydesde dentro detareafacil/rompe las importaciones relativas (attempted relative import with no known parent package). Sitúate en la carpeta de arriba y usapython -m tareafacil. - Poner código ejecutable en el nivel superior de un módulo. Recuerda que importar es ejecutar: un
input()o unprintsueltos se dispararán en cuanto alguien importe el fichero. Todo lo ejecutable, dentro de funciones y bajoif __name__ == "__main__":. - Crear ciclos entre módulos. Si
aimportabybimportaa, 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.txtque 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 pasoDos 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
- ¿Qué es la programación?
- Historia de la programación
- Lenguajes de programación
- Entornos de desarrollo
- Del problema al algoritmo
Módulo 2: Conceptos Básicos
- Variables y tipos de datos
- Operadores y expresiones
- Entrada y salida de datos
- Conversión de tipos y validación de datos
Módulo 3: Estructuras de Control
Módulo 4: Funciones y Procedimientos
- Definición y uso de funciones
- Parámetros y retorno de valores
- Ámbito de variables
- Descomponer un programa en funciones
- Funciones como valores: lambda y orden superior
Módulo 5: Estructuras de Datos
- Listas y arreglos
- Cadenas de caracteres
- Diccionarios y conjuntos
- Tuplas y estructuras anidadas
- Guardar datos en archivos: texto, CSV y JSON
Módulo 6: Algoritmos Básicos
Módulo 7: Objetos y Organización del Código
- De los datos a los objetos: clases e instancias
- Atributos, métodos y constructor
- Colecciones de objetos
- Módulos, paquetes e importaciones
Módulo 8: Buenas Prácticas y Herramientas
- Documentación y comentarios
- Depuración y manejo de errores
- Control de versiones
- Pruebas automatizadas
- Estilo, legibilidad y refactorización
