Enroll to access all the lessons in this course.
Lesson 2 of 15· Orientación: qué vas a construir
En la lección anterior conociste el objetivo: una herramienta de línea de comandos para gestionar tareas (un todo) que se instala como un comando real, taskix, y que vivirá en tu portafolio. Antes de escribir una sola línea de argparse, vamos a hacer lo que hace cualquier desarrollador con experiencia: diseñar la arquitectura primero.
Esta lección es de planos, no de código. Cuando termines tendrás un mapa mental claro de qué pieza hace qué, por dónde fluye una orden del usuario y por qué esa separación es la diferencia entre un script desechable y una aplicación que puedes mantener y probar. Todo lo que diseñemos aquí lo iremos construyendo, archivo por archivo, en los hitos siguientes.
La tentación, cuando empiezas, es poner todo en un único main.py: leer los argumentos, decidir qué hacer, abrir el archivo donde guardas las tareas, modificarlo e imprimir el resultado. Funciona... hasta que deja de funcionar.
print y un sys.exit? Imposible de reutilizar.La solución es una idea clásica y poderosa: separar en capas. Cada capa tiene una sola responsabilidad y solo conoce a su vecina inmediata.
Nuestra CLI taskix se organiza en tres capas con responsabilidades estrictamente separadas, apoyadas por dos servicios transversales (configuración y persistencia):
| Capa / módulo | Responsabilidad única | NO debe hacer |
|---|---|---|
Interfaz (cli.py) | Traducir la línea de comandos a una llamada de función: parsear argumentos con argparse, invocar al core y formatear la salida. | Contener reglas de negocio ni saber cómo se guardan los datos. |
Lógica de negocio (core.py) | Las reglas de la aplicación: crear, listar, completar y borrar tareas; validar invariantes. Funciones puras que reciben y devuelven datos. | Imprimir en pantalla, leer sys.argv ni tocar el disco directamente. |
Persistencia (storage.py) | Leer y escribir las tareas en disco (un archivo JSON). Cargar y guardar. | Aplicar reglas de negocio ni interactuar con el usuario. |
Configuración (config.py) | Resolver opciones (dónde está el archivo de datos, formato de salida) desde valores por defecto, variables de entorno y archivos. | — |
Pruebas (tests/) | Verificar cada capa por separado y la CLI completa de punta a punta con pytest. | — |
La regla de oro: la dependencia fluye en una sola dirección. La interfaz depende del core; el core depende de la persistencia a través de una interfaz simple. Nunca al revés. El core.py no sabe que existe una terminal, y por eso es trivial de probar.
Un detalle que practicaremos en todos los hitos: las funciones del core llevan anotaciones de tipo (typing). Firmas como def complete_task(tasks: list[Task], task_id: int) -> list[Task] documentan el contrato de cada función, habilitan el autocompletado del editor y permiten que herramientas como mypy detecten errores antes de ejecutar. El tipado no es decorativo: es parte de cómo una CLI seria comunica y protege sus invariantes.
Una CLI seria es predecible: dado el mismo entorno, siempre resuelve sus opciones igual. Para lograrlo fijamos desde ya un único modelo de precedencia, de menor a mayor prioridad. Lo implementaremos a fondo en el Hito 3, pero conviene tenerlo claro desde el diseño porque todo el proyecto lo respeta:
Es decir: partimos de defaults sensatos; un archivo de configuración puede sobrescribirlos; y una variable de entorno gana sobre todo lo demás (perfecta para CI o para una ejecución puntual sin editar archivos).
| Nivel | Prioridad | Ejemplo concreto en taskix |
|---|---|---|
| Valores por defecto | más baja | directorio de datos resuelto con platformdirs |
| Archivo de configuración | media | ~/.config/taskix/config.toml |
| Variable de entorno | más alta | TASKIX_HOME=/ruta/a/mis/datos |
Conviene no confundir dos rutas distintas:
tasks.json, ubicado dentro del directorio de datos. Por defecto ese directorio lo elige platformdirs (p. ej. ~/.config/taskix/ en Linux), pero TASKIX_HOME lo sobrescribe.config.toml, que ajusta opciones de comportamiento (formato de salida, etc.).¿Por qué platformdirs y no escribir ~/.config a mano? Porque esa ruta no es portable: en macOS y Windows el directorio correcto es otro. platformdirs (una dependencia que declararemos en el Hito 5) devuelve la ubicación adecuada en cada sistema operativo. Mantendremos este mismo modelo —platformdirs para los defaults, config.toml para sobrescribir, TASKIX_HOME con la última palabra— de forma consistente en todos los hitos y en el README final.
Imagina que el usuario escribe en su terminal:
taskix done 3Esto significa "marca como completada la tarea con id 3". Sigue el viaje de esa orden a través de las capas:
Léelo de arriba abajo:
cli.py recibe la línea cruda y, gracias a argparse, la convierte en datos estructurados: subcomando done, argumento id=3. Aquí no hay lógica de tareas, solo traducción.config.py le dice a la CLI dónde vive el archivo de datos: por defecto, el tasks.json dentro del directorio que resuelve platformdirs (en Linux, algo como ~/.config/taskix/tasks.json), salvo que TASKIX_HOME o config.toml indiquen otra cosa.storage.py carga la lista de tareas desde el disco.core.py recibe esa lista y el id, aplica la regla ("la tarea 3 existe y no estaba ya completada") y devuelve la lista actualizada. Es una función pura: mismos datos de entrada, mismo resultado, sin efectos secundarios sorpresa.storage.py guarda la lista modificada.cli.py formatea el resultado para el humano e indica el código de salida: 0 si todo fue bien, distinto de 0 si hubo un error.Ese último punto importa más de lo que parece: otras herramientas y scripts (&&, pipelines de CI, Makefiles) leen el exit code para saber si tu comando tuvo éxito. Lo trataremos a fondo en el Hito 3.
La propia línea taskix done 3 también tiene una estructura con nombre. Conviene fijar el vocabulario ahora, porque argparse usa exactamente estos términos. Veamos un ejemplo más rico:
taskix add "Comprar pan" --priority high --due 2026-07-01| Parte | En el ejemplo | Significado |
|---|---|---|
| Programa | taskix | El comando instalado; el entry point de tu paquete. |
| Subcomando | add | La acción a ejecutar. Cada subcomando (add, list, done, remove) es como un mini-programa con sus propios argumentos. |
| Argumento posicional | "Comprar pan" | Un valor requerido cuya posición determina su significado (aquí, el título de la tarea). |
| Opción / flag con valor | --priority high, --due 2026-07-01 | Parámetros con nombre, normalmente opcionales, que ajustan el comportamiento. El orden no importa. |
| Flag booleano | (p. ej. --verbose) | Una opción sin valor: está presente o no. Activa o desactiva algo. |
Distinguir posicional (significado por posición) de opción (significado por nombre, con --) es la base de todo lo que harás con argparse en el Hito 1. Y la idea de subcomandos —que git commit y git push son acciones distintas bajo el mismo programa git— es justo el patrón que replicaremos en el Hito 2.
Cada capa será un módulo de Python dentro de un paquete instalable. Así se verá el repositorio taskix que vas a construir:
Algunas decisiones de esta estructura que vale la pena entender desde ya:
src/. Poner el código dentro de src/taskix/ (en lugar de en la raíz) evita un error clásico: que tus pruebas importen el código "por accidente" desde el directorio actual en vez de desde el paquete instalado. Con src/ te ves obligado a instalar el paquete (pip install -e .) y pruebas lo que de verdad se distribuye. Lo configuraremos en el Hito 5.pyproject.toml es el archivo único y moderno que describe tu paquete: nombre, versión, dependencias (entre ellas platformdirs) y —clave— el entry point que convierte taskix.cli:main en el comando taskix. Es la pieza que transforma tu script en algo que se instala.tests/ separado, con un archivo de pruebas por módulo. Como cada capa está desacoplada, test_core.py puede probar las reglas de negocio sin tocar disco ni terminal, y test_cli.py puede ejecutar la CLI completa capturando su salida y su exit code. Esto es exactamente lo que hace fácil la separación en capas.No es ceremonia: cada decisión compra algo concreto.
core.py son funciones puras con tipos explícitos que reciben una lista de tareas y devuelven otra. Probar "no puedo completar dos veces la misma tarea" es una sola línea de assert, sin archivos ni subprocesos. Las pruebas lentas (disco, terminal) quedan confinadas a las capas que de verdad tocan el mundo exterior.storage.py; el core y la CLI ni se enteran, porque dependen del qué (guardar tareas), no del cómo. ¿Cambiar el formato de salida a una tabla bonita? Solo tocas cli.py.core.py y storage.py tal cual, y solo escribes una nueva capa de interfaz. La lógica de negocio no está "atrapada" dentro de la terminal.cli.py, las reglas en core.py, el disco en storage.py. Un futuro tú (o un reclutador mirando tu portafolio) lo agradecerá.Una heurística útil mientras codificas los próximos hitos: si un print o un sys.exit se cuela en core.py, o una regla de negocio aparece en cli.py, algo está mal de capa. Mantén cada pieza en su sitio.
cli.py, argparse), lógica de negocio (core.py, funciones puras con anotaciones de tipo) y persistencia (storage.py), apoyadas por configuración (config.py) y pruebas (tests/).platformdirs para los defaults, config.toml para sobrescribir y TASKIX_HOME con la última palabra.argparse.src/ + pyproject.toml con un entry point convierte tu proyecto taskix en un paquete instalable y probado de verdad.En la próxima lección abrimos el editor: construirás tu primer comando ejecutable con argparse y verás esta arquitectura empezar a cobrar vida.
Free