Una herramienta de línea de comandos para gestionar tareas: órdenes como «añadir», «listar», «hecha» y «borrar», prioridades, fechas límite y los datos guardados en JSON.
4 pasos
1 hora y media aproximadamente
87 líneas de código
Lo que vas a construir
Muchas herramientas que usan los programadores cada día (git, npm, pip) funcionan con órdenes en la terminal. Vas a crear la tuya: un gestor de tareas con subcomandos, ayuda automática y datos guardados en un archivo JSON, usando solo la biblioteca estándar de Python.
Lo que vas a practicar
Clases de datos con @dataclass
Guardar objetos en JSON
Subcomandos con argparse
Validar los argumentos
Ordenar con varias claves y trabajar con fechas
Cómo ejecutarlo: Guarda el código como tareas.py y úsalo desde la terminal: python tareas.py añadir "Llamar al médico", python tareas.py listar, python tareas.py hecha 1. Con python tareas.py --help verás todas las órdenes.
Así funciona al final
$ python tareas.py añadir "Renovar el DNI" -p alta --hasta 2025-12-01
Añadida: Renovar el DNI
$ python tareas.py añadir "Leer un libro" -p baja
Añadida: Leer un libro
$ python tareas.py añadir "Preparar la mudanza" -p alta --hasta 2030-06-30
Añadida: Preparar la mudanza
$ python tareas.py listar
1. [ ] alta Renovar el DNI (hasta el 2025-12-01) ¡VENCIDA!
3. [ ] alta Preparar la mudanza (hasta el 2030-06-30)
2. [ ] baja Leer un libro
Antes de empezar, conviene haber hecho estas lecciones
@dataclass crea una clase para guardar datos con su constructor ya hecho: Tarea("Comprar pan") tiene hecha=False por defecto. asdict la convierte en diccionario para guardarla en JSON y Tarea(**d) la vuelve a crear al cargar.
pathlib.Path maneja la ruta del archivo; si todavía no existe, cargar devuelve una lista vacía. Cada vez que ejecutes este paso se añadirá otra tarea.
tareas.py
import json
from dataclasses import dataclass, asdict
from pathlib import Path
ARCHIVO = Path("tareas.json")
@dataclass
class Tarea:
texto: str
hecha: bool = False
def cargar():
if not ARCHIVO.exists():
return []
datos = json.loads(ARCHIVO.read_text(encoding="utf-8"))
return [Tarea(**d) for d in datos]
def guardar(tareas):
datos = [asdict(t) for t in tareas]
ARCHIVO.write_text(json.dumps(datos, ensure_ascii=False, indent=2), encoding="utf-8")
tareas = cargar()
tareas.append(Tarea("Comprar pan"))
guardar(tareas)
print(f"Hay {len(tareas)} tareas guardadas en {ARCHIVO}")
Al ejecutarlo
Hay 1 tareas guardadas en tareas.json
2
Paso 2 de 4
Órdenes con argparse
add_subparsers crea órdenes como las de git: python tareas.py añadir "texto" y python tareas.py listar. Cada orden tiene sus propios argumentos y su ayuda.
enumerate(tareas, start=1) numera las tareas desde 1 al listarlas. El bloque if __name__ == "__main__": hace que main() solo se ejecute al lanzar el archivo, no si otro programa lo importa.
tareas.pyLo nuevo, resaltado
import argparse
import json
from dataclasses import dataclass, asdict
from pathlib import Path
ARCHIVO = Path("tareas.json")
@dataclass
class Tarea:
texto: str
hecha: bool = False
def cargar():
if not ARCHIVO.exists():
return []
datos = json.loads(ARCHIVO.read_text(encoding="utf-8"))
return [Tarea(**d) for d in datos]
def guardar(tareas):
datos = [asdict(t) for t in tareas]
ARCHIVO.write_text(json.dumps(datos, ensure_ascii=False, indent=2), encoding="utf-8")
def listar(tareas):
if not tareas:
print("No hay tareas. Añade una con: python tareas.py añadir \"texto\"")
return
for numero, tarea in enumerate(tareas, start=1):
marca = "x" if tarea.hecha else " "
print(f"{numero}. [{marca}] {tarea.texto}")
def main():
parser = argparse.ArgumentParser(prog="tareas.py", description="Tus tareas en la terminal")
ordenes = parser.add_subparsers(dest="orden", required=True)
nueva = ordenes.add_parser("añadir", help="añade una tarea")
nueva.add_argument("texto")
ordenes.add_parser("listar", help="muestra las tareas")
args = parser.parse_args()
tareas = cargar()
if args.orden == "añadir":
tareas.append(Tarea(args.texto))
guardar(tareas)
print(f"Añadida: {args.texto}")
elif args.orden == "listar":
listar(tareas)
if __name__ == "__main__":
main()
Al ejecutarlo
$ python tareas.py añadir "Comprar pan"
Añadida: Comprar pan
$ python tareas.py añadir "Llamar al médico"
Añadida: Llamar al médico
$ python tareas.py listar
1. [ ] Comprar pan
2. [ ] Llamar al médico
3
Paso 3 de 4
Marcar y borrar con comprobaciones
Las órdenes hecha y borrar reciben el número de una tarea. type=int hace que argparse lo convierta y avise si no es un número.
Si el número no corresponde a ninguna tarea, parser.error muestra el uso correcto y un mensaje claro, y termina con un código de error. Nunca se modifica la lista con datos que no se han comprobado.
tareas.pyLo nuevo, resaltado
import argparse
import json
from dataclasses import dataclass, asdict
from pathlib import Path
ARCHIVO = Path("tareas.json")
@dataclass
class Tarea:
texto: str
hecha: bool = False
def cargar():
if not ARCHIVO.exists():
return []
datos = json.loads(ARCHIVO.read_text(encoding="utf-8"))
return [Tarea(**d) for d in datos]
def guardar(tareas):
datos = [asdict(t) for t in tareas]
ARCHIVO.write_text(json.dumps(datos, ensure_ascii=False, indent=2), encoding="utf-8")
def listar(tareas):
if not tareas:
print("No hay tareas. Añade una con: python tareas.py añadir \"texto\"")
return
for numero, tarea in enumerate(tareas, start=1):
marca = "x" if tarea.hecha else " "
print(f"{numero}. [{marca}] {tarea.texto}")
def main():
parser = argparse.ArgumentParser(prog="tareas.py", description="Tus tareas en la terminal")
ordenes = parser.add_subparsers(dest="orden", required=True)
nueva = ordenes.add_parser("añadir", help="añade una tarea")
nueva.add_argument("texto")
ordenes.add_parser("listar", help="muestra las tareas")
for nombre, ayuda in (("hecha", "marca una tarea como hecha"), ("borrar", "borra una tarea")):
orden = ordenes.add_parser(nombre, help=ayuda)
orden.add_argument("numero", type=int, help="número de la tarea (lo ves al listar)")
args = parser.parse_args()
tareas = cargar()
if args.orden == "añadir":
tareas.append(Tarea(args.texto))
guardar(tareas)
print(f"Añadida: {args.texto}")
elif args.orden == "listar":
listar(tareas)
else:
# hecha y borrar reciben un número: hay que comprobar que existe
if not 1 <= args.numero <= len(tareas):
parser.error(f"no hay ninguna tarea {args.numero} (tienes {len(tareas)})")
tarea = tareas[args.numero - 1]
if args.orden == "hecha":
tarea.hecha = True
print(f"Hecha: {tarea.texto}")
else:
tareas.remove(tarea)
print(f"Borrada: {tarea.texto}")
guardar(tareas)
if __name__ == "__main__":
main()
Al ejecutarlo
$ python tareas.py añadir "Comprar pan"
Añadida: Comprar pan
$ python tareas.py añadir "Llamar al médico"
Añadida: Llamar al médico
$ python tareas.py hecha 1
Hecha: Comprar pan
$ python tareas.py borrar 5
usage: tareas.py [-h] {añadir,listar,hecha,borrar} ...
tareas.py: error: no hay ninguna tarea 5 (tienes 2)
$ python tareas.py listar
1. [x] Comprar pan
2. [ ] Llamar al médico
4
Paso 4 de 4
Prioridades y fechas límite
La tarea gana dos campos con valor por defecto: la prioridad (choices hace que argparse solo admita alta, media o baja) y una fecha límite que date.fromisoformat comprueba al leerla. El método vencida compara esa fecha con la de hoy.
Al listar se ordena con una tupla como clave: primero las pendientes y, dentro de ellas, por prioridad. Cada tarea conserva el número de la lista guardada, que es el que usan hecha y borrar. Los archivos de los pasos anteriores siguen funcionando, porque los campos nuevos tienen valor por defecto.
tareas.pyLo nuevo, resaltado
import argparse
import json
from dataclasses import dataclass, asdict
from datetime import date
from pathlib import Path
ARCHIVO = Path("tareas.json")
PRIORIDADES = {"alta": 0, "media": 1, "baja": 2}
@dataclass
class Tarea:
texto: str
hecha: bool = False
prioridad: str = "media"
limite: str | None = None # fecha AAAA-MM-DD o None
def vencida(self):
return not self.hecha and self.limite is not None and date.fromisoformat(self.limite) < date.today()
def cargar():
if not ARCHIVO.exists():
return []
datos = json.loads(ARCHIVO.read_text(encoding="utf-8"))
return [Tarea(**d) for d in datos]
def guardar(tareas):
datos = [asdict(t) for t in tareas]
ARCHIVO.write_text(json.dumps(datos, ensure_ascii=False, indent=2), encoding="utf-8")
def listar(tareas):
if not tareas:
print("No hay tareas. Añade una con: python tareas.py añadir \"texto\"")
return
# Primero las pendientes; dentro, por prioridad. El número sigue siendo el de la lista guardada.
orden = sorted(enumerate(tareas, start=1), key=lambda par: (par[1].hecha, PRIORIDADES[par[1].prioridad]))
for numero, tarea in orden:
marca = "x" if tarea.hecha else " "
extra = f" (hasta el {tarea.limite})" if tarea.limite else ""
aviso = " ¡VENCIDA!" if tarea.vencida() else ""
print(f"{numero}. [{marca}] {tarea.prioridad:<5} {tarea.texto}{extra}{aviso}")
def main():
parser = argparse.ArgumentParser(prog="tareas.py", description="Tus tareas en la terminal")
ordenes = parser.add_subparsers(dest="orden", required=True)
nueva = ordenes.add_parser("añadir", help="añade una tarea")
nueva.add_argument("texto")
nueva.add_argument("-p", "--prioridad", choices=PRIORIDADES, default="media")
nueva.add_argument("--hasta", type=date.fromisoformat, help="fecha límite, por ejemplo 2026-05-31")
ordenes.add_parser("listar", help="muestra las tareas")
for nombre, ayuda in (("hecha", "marca una tarea como hecha"), ("borrar", "borra una tarea")):
orden = ordenes.add_parser(nombre, help=ayuda)
orden.add_argument("numero", type=int, help="número de la tarea (lo ves al listar)")
args = parser.parse_args()
tareas = cargar()
if args.orden == "añadir":
limite = args.hasta.isoformat() if args.hasta else None
tareas.append(Tarea(args.texto, prioridad=args.prioridad, limite=limite))
guardar(tareas)
print(f"Añadida: {args.texto}")
elif args.orden == "listar":
listar(tareas)
else:
# hecha y borrar reciben un número: hay que comprobar que existe
if not 1 <= args.numero <= len(tareas):
parser.error(f"no hay ninguna tarea {args.numero} (tienes {len(tareas)})")
tarea = tareas[args.numero - 1]
if args.orden == "hecha":
tarea.hecha = True
print(f"Hecha: {tarea.texto}")
else:
tareas.remove(tarea)
print(f"Borrada: {tarea.texto}")
guardar(tareas)
if __name__ == "__main__":
main()
Al ejecutarlo
$ python tareas.py añadir "Renovar el DNI" -p alta --hasta 2025-12-01
Añadida: Renovar el DNI
$ python tareas.py añadir "Leer un libro" -p baja
Añadida: Leer un libro
$ python tareas.py añadir "Preparar la mudanza" -p alta --hasta 2030-06-30
Añadida: Preparar la mudanza
$ python tareas.py listar
1. [ ] alta Renovar el DNI (hasta el 2025-12-01) ¡VENCIDA!
3. [ ] alta Preparar la mudanza (hasta el 2030-06-30)
2. [ ] baja Leer un libro
Retos para seguir
Has terminado el proyecto. Si quieres ir más allá, prueba a añadirle esto por tu cuenta:
Añade una orden editar que cambie el texto o la prioridad de una tarea.
Escribe pruebas con unittest para cargar, guardar y el orden de listar.
Colorea en rojo las tareas vencidas con los códigos de color ANSI de la terminal.
Con tu permiso usamos Google Analytics, que guarda cookies, para saber cuántas personas visitan la web y qué páginas les resultan útiles. No usamos cookies de publicidad y tus archivos nunca salen de tu navegador. Puedes cambiar de opinión cuando quieras en «Configurar cookies», al pie de la página. Más información.