Con el DEFINICION.md de Definición del proyecto y el DISENO.md de Planificación y diseño sobre la mesa, empieza la parte que llevabas esperando: escribir el programa. Y aquí es donde se pierden la mayoría de los primeros proyectos, no por falta de conocimientos —los tienes todos— sino por método. Quien empieza suele abrir los cinco ficheros a la vez, escribir cuatrocientas líneas sin ejecutar nada y encontrarse con treinta errores encadenados que ya no sabe por dónde coger. Esta lección enseña lo contrario: construir en incrementos pequeños, con el programa siempre ejecutable, probando y haciendo commit a cada paso, y con un procedimiento concreto para los dos momentos difíciles —cuando algo falla y no sabes por qué, y cuando te bloqueas y no sabes seguir—.
Contenido
- La estrategia: incremental y siempre ejecutable
- Orden de construcción
- Fase 1: el esqueleto que arranca
- Fase 2: el modelo y sus pruebas
- Fase 3: el almacén y su prueba de ida y vuelta
- Fase 4: la lógica de la colección
- Fase 5: la interfaz y su prueba manual
- Cuando algo no funciona: depurar de verdad
- Gestión de los errores del usuario
- Commits durante el desarrollo
- Cuando te bloqueas
- Checklist de «terminado»
- Errores comunes y consejos
- Ejercicios
- Conclusión
- La estrategia: incremental y siempre ejecutable
La regla es una sola frase: el programa debe poder ejecutarse en todo momento, desde el minuto uno —cuando solo muestra un menú vacío— hasta la v1.0. Nunca hay un estado intermedio de «está a medias, no arranca». Compara las dos formas de trabajar. En el enfoque «todo a la vez» escribes los cinco módulos antes de ejecutar, el primer arranque saca treinta errores encadenados sin que sepas cuál causa cuál, el commit es gigante y no hay nada que enseñar hasta el final. En el enfoque incremental escribes lo mínimo y ejecutas, cada error aparece solo y sobre el código que acabas de tocar, hay un commit por incremento y algo que funciona desde el primer día.
La diferencia de fondo está en el segundo punto. Cuando un fallo aparece justo después de escribir diez líneas, el sospechoso son esas diez líneas. Cuando aparece después de cuatrocientas, el sospechoso es todo el programa, y depurar pasa de cinco minutos a dos horas.
El ciclo de trabajo, entonces, es este bucle corto:
flowchart LR
E["Escribir<br/>un incremento pequeno"] --> R["Ejecutar<br/>python -m misgastos"]
R --> P["Probar<br/>pytest"]
P --> C["Commit<br/>mensaje descriptivo"]
C --> E
R -->|falla| D["Depurar"]
D --> R
P -->|falla| D
Un giro completo de ese bucle debería durar entre quince minutos y una hora: si llevas tres horas sin volver a la casilla «Ejecutar», el incremento era demasiado grande.
- Orden de construcción
El orden no es indiferente: se construye de dentro hacia fuera, porque cada capa depende de la anterior y porque las capas interiores son las que se pueden probar automáticamente.
| Fase | Qué construyes | Cómo lo compruebas | Est. |
|---|---|---|---|
| 1. Esqueleto | __main__.py e interfaz.py con el menú y la salida |
Arranca y sale | 1 h |
| 2. Modelo | modelo.py: entidad, validación, excepción |
pytest tests/test_modelo.py |
2 h |
| 3. Almacén | almacen.py: guardar y cargar |
pytest con tmp_path |
2 h |
| 4. Lógica | cuaderno.py: agregar, buscar, filtrar, totales |
pytest tests/test_cuaderno.py |
3 h |
| 5. Interfaz | Las opciones del menú, una a una | Guion de prueba manual | 6 h |
| 6. Extras | Should, robustez, logging, formato |
Guion + revisión final | 4 h |
Dos advertencias sobre este orden. La primera: aunque la interfaz sea lo último de la lista, el hito de la primera funcionalidad completa de punta a punta llega antes de terminar todas las capas. En cuanto tengas modelo, almacén y un agregar en la lógica, haz la opción «registrar» del menú entera; ver el programa hacer algo real cambia la motivación más que cualquier otra cosa. Y la segunda: las fases 2, 3 y 4 se prueban automáticamente; la 5, no. Probar una interfaz de línea de comandos automáticamente es posible pero costoso, y no es lo que corresponde a un primer proyecto: para ella se usa el guion manual del apartado 7.
- Fase 1: el esqueleto que arranca
El primer objetivo es que python -m misgastos funcione. Nada más.
# misgastos/interfaz.py -- capa de presentacion
OPCIONES = {"1": "Registrar gasto", "2": "Registrar ingreso", "3": "Movimientos del mes",
"4": "Resumen por categoria", "5": "Borrar movimiento", "0": "Salir"}
def ejecutar() -> None:
"""Bucle principal de la aplicacion."""
while True:
print("\n=== MisGastos ===")
for clave, texto in OPCIONES.items():
print(f" {clave}. {texto}")
opcion = input("Opcion: ").strip()
if opcion == "0":
print("Hasta luego.")
break
print(f"[pendiente] {OPCIONES[opcion]}" if opcion in OPCIONES else "Opcion no valida.")
# misgastos/__main__.py -> from .interfaz import ejecutar; ejecutar()Son veinte líneas y ninguna hace nada útil todavía, pero resuelven de golpe los tres problemas que más bloquean al principio: que el paquete se importe bien, que la ejecución con -m funcione y que exista el bucle principal de Menús interactivos. El [pendiente] de cada opción es deliberado: el menú completo existe desde el primer día y las opciones se van rellenando una a una. Commit: Esqueleto: menu principal que arranca y sale.
- Fase 2: el modelo y sus pruebas
Ahora la entidad, con toda su validación dentro. Es la pieza más importante del programa: si el modelo garantiza que no puede existir un movimiento inválido, ninguna otra capa tiene que preocuparse de comprobarlo.
# misgastos/modelo.py -- entidades del dominio y sus reglas de validacion
CATEGORIAS = ("comida", "transporte", "ocio", "hogar", "salud", "ingresos", "otros")
class MovimientoInvalido(ValueError):
"""Se lanza cuando los datos de un movimiento no cumplen las reglas."""
@dataclass
class Movimiento:
"""Un ingreso o gasto. El signo del importe distingue uno de otro."""
fecha: date
concepto: str
importe: float
categoria: str
id: int = 0
def __post_init__(self) -> None:
self.concepto = self.concepto.strip() # normalizar...
self.categoria = self.categoria.strip().lower()
if not self.concepto or len(self.concepto) > 60: # ...y despues validar
raise MovimientoInvalido("El concepto debe tener entre 1 y 60 caracteres.")
if self.importe == 0:
raise MovimientoInvalido("El importe no puede ser cero.")
if self.categoria not in CATEGORIAS:
raise MovimientoInvalido(f"Categoria desconocida: {self.categoria!r}.")
if self.fecha > date.today():
raise MovimientoInvalido("La fecha no puede ser futura.")
def es_gasto(self) -> bool:
return self.importe < 0
def __str__(self) -> str: # linea ya formateada de tabla
return f"{self.id:>4} {self.fecha} {self.concepto:<30} {self.importe:>10.2f}"Las decisiones que hay ahí, una a una. @dataclass (07-02) evita escribir un __init__ que solo asignaría cinco atributos. __post_init__ es el método que dataclass llama justo después de construir, y es donde va la validación. Se normaliza antes de validar —strip() y lower()— para que « Comida » y «comida» sean lo mismo, que era uno de los casos límite previstos en 09-01. La excepción propia hereda de ValueError porque es un error de valor: quien no conozca MovimientoInvalido puede capturar ValueError y funcionará igual. Y __str__ devuelve ya la línea formateada de tabla, con los anchos fijos del esbozo de pantalla.
Las pruebas, con el patrón AAA de Pruebas automatizadas:
# tests/test_modelo.py
def test_normaliza_concepto_y_categoria():
m = Movimiento(date(2026, 8, 1), " Cafe ", -1.5, " COMIDA ")
assert m.concepto == "Cafe" and m.categoria == "comida" and m.es_gasto()
@pytest.mark.parametrize("concepto, importe, categoria", [
("", -10.0, "comida"), # concepto vacio
("Compra", 0.0, "comida"), # importe cero
("Compra", -10.0, "viajes"), # categoria desconocida
])
def test_datos_invalidos(concepto, importe, categoria):
with pytest.raises(MovimientoInvalido):
Movimiento(date(2026, 8, 1), concepto, importe, categoria)
def test_fecha_futura_rechazada():
manana = date.today() + timedelta(days=1) # calculada, nunca fija
with pytest.raises(MovimientoInvalido):
Movimiento(manana, "Compra", -10.0, "comida")Fíjate en qué se prueba: un caso bueno, la normalización y todos los casos malos. Los tres inválidos van en un parametrize porque son la misma prueba con datos distintos, y pytest los cuenta como tres pruebas separadas, así que si falla una sabes cuál. La de la fecha futura calcula «mañana» en lugar de escribir una fecha fija: una prueba con date(2027, 1, 1) empezaría a fallar sola en 2027, y una prueba que caduca es peor que no tenerla. Commit: Modelo Movimiento con validacion y pruebas (RF-1).
- Fase 3: el almacén y su prueba de ida y vuelta
guardar es la parte fácil: construye el diccionario con la estructura que fijaste en 09-02 —version, siguiente_id y la lista de movimientos convertidos con m.fecha.isoformat()— y lo escribe con ruta.write_text(json.dumps(datos, indent=2, ensure_ascii=False), encoding="utf-8"). La parte que hay que hacer bien es la lectura:
# misgastos/almacen.py (RUTA = Path("gastos.json"), logger = getLogger(__name__))
def cargar(ruta: Path = RUTA) -> tuple[list[Movimiento], int]:
"""Lee los movimientos guardados. Devuelve lista vacia si no hay fichero."""
if not ruta.exists():
return [], 1 # primer arranque: no es un error
try:
datos = json.loads(ruta.read_text(encoding="utf-8"))
except json.JSONDecodeError:
logger.error("Fichero de datos corrupto: %s", ruta)
return [], 1 # se registra y se sigue, no se muere
movimientos = [
Movimiento(date.fromisoformat(d["fecha"]), d["concepto"],
d["importe"], d["categoria"], id=d["id"])
for d in datos.get("movimientos", [])
]
return movimientos, datos.get("siguiente_id", len(movimientos) + 1)Lo que hay que mirar aquí es la tolerancia a fallos, exactamente igual que en el almacen.py de TareaFácil: el fichero ausente es el primer arranque y el corrupto se registra en el log (08-02) en lugar de matar el programa. Y el parámetro ruta con valor por defecto no es un capricho: es lo que permite que las pruebas usen un fichero temporal.
# tests/test_almacen.py
def test_ciclo_guardar_y_cargar(tmp_path):
ruta = tmp_path / "datos.json"
originales = [Movimiento(date(2026, 8, 1), "Compra", -62.35, "comida", id=1),
Movimiento(date(2026, 8, 3), "Nomina", 1450.0, "ingresos", id=2)]
guardar(originales, siguiente_id=3, ruta=ruta)
recuperados, siguiente = cargar(ruta)
assert siguiente == 3 and len(recuperados) == 2
assert recuperados[0].importe == -62.35 # sigue siendo float
assert recuperados[1].fecha == date(2026, 8, 3) # sigue siendo date
def test_fichero_inexistente_no_falla(tmp_path):
assert cargar(tmp_path / "no_existe.json") == ([], 1)
def test_fichero_corrupto_no_falla(tmp_path):
(tmp_path / "roto.json").write_text("{no es json", encoding="utf-8")
assert cargar(tmp_path / "roto.json")[0] == []La primera es la prueba de ida y vuelta: guardo, cargo y compruebo que lo recuperado es idéntico a lo original. Es la prueba más rentable de toda la persistencia, porque un solo assert detecta fallos de serialización, de tipos y de estructura. La fixture tmp_path de pytest da un directorio temporal distinto por prueba y lo borra al terminar, así que las pruebas nunca tocan tus datos reales ni se estorban entre sí. Commit: Almacen JSON tolerante a fichero ausente o corrupto (RF-6).
- Fase 4: la lógica de la colección
# misgastos/cuaderno.py -- coleccion de movimientos y operaciones sobre ella
class Cuaderno:
"""Guarda los movimientos y responde consultas sobre ellos."""
def __init__(self, movimientos: list[Movimiento] | None = None, siguiente_id: int = 1):
self._movimientos = movimientos or []
self.siguiente_id = siguiente_id
def agregar(self, movimiento: Movimiento) -> Movimiento:
"""Asigna identificador al movimiento y lo anade al cuaderno."""
movimiento.id = self.siguiente_id
self.siguiente_id += 1
self._movimientos.append(movimiento)
return movimiento
def del_mes(self, mes: str) -> list[Movimiento]:
"""Movimientos de un mes 'AAAA-MM', ordenados por fecha."""
del_mes = [m for m in self._movimientos if m.fecha.isoformat().startswith(mes)]
return sorted(del_mes, key=lambda m: m.fecha)
def total_por_categoria(self, mes: str) -> dict[str, float]:
"""Suma los importes de cada categoria en el mes, de mayor gasto a menor."""
totales: dict[str, float] = {}
for m in self.del_mes(mes):
totales[m.categoria] = totales.get(m.categoria, 0.0) + m.importe
return dict(sorted(totales.items(), key=lambda par: par[1]))
def saldo(self, mes: str) -> float:
"""Diferencia entre ingresos y gastos del mes."""
return round(sum(m.importe for m in self.del_mes(mes)), 2)Es la traducción literal del pseudocódigo de 09-02 (faltan __len__, __iter__, buscar —un next() sobre la lista que devuelve None si no hay coincidencia— y eliminar, que usa buscar y devuelve True o False), y merece la pena señalar tres cosas. total_por_categoria reutiliza del_mes en lugar de repetir el filtro: una responsabilidad, un sitio. El key=lambda par: par[1] ordena por valor, y como los gastos son negativos, el orden ascendente pone primero el mayor gasto, que es lo que quería el esbozo de pantalla (06-02 y 04-05 trabajando juntos). Y el round(..., 2) de saldo evita que asome la aritmética de coma flotante: sin él, -62.35 + 1450.0 puede imprimir 1387.6500000000001.
Las pruebas de esta capa se centran en los cálculos, que es donde de verdad puede haber un error. Y aquí no hay que inventar nada, porque cada tabla de prueba de escritorio de 09-02 se convierte en un def test_: los mismos cuatro movimientos de aquella tabla, el mismo mes y el mismo resultado esperado.
def test_total_por_categoria_agrupa_y_filtra_el_mes():
c = Cuaderno()
c.agregar(Movimiento(date(2026, 7, 30), "Julio", -20.0, "comida")) # otro mes
c.agregar(Movimiento(date(2026, 8, 1), "Compra", -62.35, "comida"))
c.agregar(Movimiento(date(2026, 8, 4), "Cena", -15.0, "comida"))
c.agregar(Movimiento(date(2026, 8, 1), "Abono", -32.0, "transporte"))
totales = c.total_por_categoria("2026-08")
assert totales == {"comida": -77.35, "transporte": -32.0}
assert list(totales)[0] == "comida" # el mayor gasto va primeroCommit: Cuaderno: alta, baja, filtro por mes y totales (RF-3, RF-4, RF-5).
- Fase 5: la interfaz y su prueba manual
La interfaz se rellena una opción cada vez, y cada opción se termina del todo antes de empezar la siguiente. Su regla es la de 04-04: aquí se hace input y print, y nada más; ningún cálculo del dominio.
def registrar_gasto(cuaderno: Cuaderno) -> None:
"""Opcion 1 del menu: da de alta un gasto."""
concepto = input("Concepto: ")
importe = pedir_importe("Importe (EUR): ")
categoria = input(f"Categoria {CATEGORIAS}: ")
texto_fecha = input("Fecha AAAA-MM-DD (vacio = hoy): ").strip()
try:
fecha = date.fromisoformat(texto_fecha) if texto_fecha else date.today()
movimiento = cuaderno.agregar(Movimiento(fecha, concepto, -importe, categoria))
except (ValueError, MovimientoInvalido) as error:
print(f"No se ha registrado: {error}")
return
print(f"Gasto registrado (id {movimiento.id}).")Tres detalles con intención. pedir_importe (un bucle while True con try/except ValueError que solo devuelve cuando el valor es mayor que cero) insiste hasta obtener un número válido en lugar de rendirse (02-04), y hace .replace(",", ".") para aceptar la coma decimal española, que era otro caso límite previsto. Se pide el importe en positivo y se guarda como -importe: al usuario no se le puede exigir que entienda el convenio de signos del diseño. Y el try/except captura tanto ValueError (fecha mal escrita) como MovimientoInvalido (reglas del modelo) y vuelve al menú con un mensaje, cumpliendo el RNF-1.
Como esto no se prueba automáticamente, se prueba con un guion escrito que ejecutas entero antes de dar por buena cada sesión de trabajo:
| # | Qué hago | Qué debe pasar | ¿OK? |
|---|---|---|---|
| 1 | Borro gastos.json y arranco |
Arranca sin error, cuaderno vacío | |
| 2 | Opción 3 sin datos | «Sin movimientos en 2026-08», no una tabla vacía | |
| 3 | Registro un gasto correcto | Confirma con id 1 | |
| 4 | Registro con importe doce y luego 12,50 |
Vuelve a pedirlo sin romperse; después lo acepta como 12,50 | |
| 5 | Registro con categoría viajes |
Mensaje de categoría desconocida, vuelve al menú | |
| 6 | Registro con fecha 2026-02-31 |
Mensaje de fecha inválida, vuelve al menú | |
| 7 | Salgo y vuelvo a entrar | Los movimientos siguen ahí | |
| 8 | Borro el id 99 y luego uno real | «No existe el movimiento 99» sin traceback; el real desaparece del fichero | |
| 9 | Opción 7 y luego abc |
«Opción no válida» en ambos casos | |
| 10 | Opción 4 con datos de dos meses | Solo suma el mes pedido; porcentajes cuadran a 100 % |
Ese guion es tu red de seguridad para lo que las pruebas automáticas no cubren. Guárdalo en tests/guion_manual.md y recórrelo entero antes de cada etiqueta de versión.
- Cuando algo no funciona: depurar de verdad
Tarde o temprano algo falla y no sabes por qué. Aplica el método de Depuración y manejo de errores a un caso real: el resumen de agosto muestra un porcentaje de 137 %.
| Paso | Qué se hace | En este fallo concreto |
|---|---|---|
| 1. Reproducir | Encontrar la receta exacta que lo provoca siempre | Con datos_ejemplo.json, opción 4, mes 2026-08 |
| 2. Leer el error | El traceback se lee de abajo arriba: tipo, fichero y línea; la primera línea de tus ficheros es la sospechosa | Aquí no hay traceback: es un resultado incorrecto, el caso más difícil |
| 3. Hipótesis concreta | Una frase falsable, no «algo falla» | «El denominador incluye los ingresos, que son positivos, y por eso el total es menor de lo que debería» |
| 4. Comprobarla | Punto de interrupción en la línea sospechosa e inspección de variables | totales incluye "ingresos": 1450.0 y total_gastos vale 892.9: confirmada |
| 5. Arreglar la causa | Nunca el síntoma | sum(totales.values()) → sum(v for v in totales.values() if v < 0) |
| 6. Prueba de regresión | La prueba que habría fallado antes del arreglo | assert total_gastos == -100.0 con dos gastos y un ingreso |
El paso 5 merece énfasis: el arreglo malo habría sido restar 1450 en algún sitio para que el número cuadrase. Habría funcionado con esos datos y habría vuelto a fallar con otros. Y el paso 6 no es opcional: un fallo arreglado sin prueba vuelve, normalmente en la refactorización siguiente. Commit: Corrige el porcentaje del resumen, que incluia los ingresos.
- Gestión de los errores del usuario
Antes de cerrar la implementación, haz un repaso sistemático: lista todas las entradas del usuario y decide para cada una qué haces. Hay dos estrategias y conviene no confundirlas. Validar es comprobar antes de actuar y volver a pedir el dato: sirve para lo que el usuario puede corregir. Capturar es dejar que la operación falle y atrapar la excepción con try/except: sirve para lo que no se puede comprobar por adelantado sin duplicar el trabajo.
| Entrada | Riesgo | Estrategia | Dónde |
|---|---|---|---|
| Opción del menú | Texto o número fuera de rango | Validar contra el diccionario | interfaz.ejecutar |
| Importe | No numérico, cero, negativo, con coma | Validar en bucle | interfaz.pedir_importe |
| Fecha y mes | Formato o día imposible | Capturar ValueError |
interfaz.registrar_gasto |
| Categoría | Desconocida, mayúsculas, espacios | Normalizar y validar | modelo.Movimiento |
| Id a borrar | No numérico o inexistente | Capturar + comprobar None |
interfaz + cuaderno.buscar |
| Fichero de datos | Ausente, corrupto, sin permisos | Capturar y arrancar vacío | almacen.cargar |
La regla de oro está en la última columna: la validación de las reglas del dominio vive en el modelo, la del formato de entrada vive en la interfaz. Si mezclas las dos, acabas validando la categoría en tres sitios distintos y olvidándote de uno.
- Commits durante el desarrollo
Un commit es una unidad de trabajo con sentido: algo que funciona y que podrías explicar en una frase. Ni cada Ctrl+S ni una vez por semana. En la práctica, uno por giro del bucle del apartado 1.
$ git log --oneline
a91c02f Anade exportacion a CSV del mes (RF-7)
7d4e1b8 Corrige el porcentaje del resumen, que incluia los ingresos
3f2a90c Anade pantalla de resumen por categoria (RF-4)
c18b7e5 Cuaderno: alta, baja, filtro por mes y totales (RF-3, RF-5)
9e04a13 Almacen JSON tolerante a fichero ausente o corrupto (RF-6)
2a1f6e0 Esqueleto: menu principal que arranca y saleEse historial se lee como el diario del proyecto: cada línea es un incremento, cada mensaje empieza por un verbo y varios citan el requisito que implementan. Tres reglas prácticas: nunca hagas commit con las pruebas en rojo (si necesitas guardar a medias, usa una rama), no mezcles refactorización con funcionalidad nueva en el mismo commit (08-05), y no guardes datos personales ni el entorno virtual, para eso está el .gitignore de 09-02.
- Cuando te bloqueas
Bloquearse es normal y le pasa a todo el mundo. Lo que distingue a quien avanza es tener un procedimiento en lugar de mirar la pantalla:
- El patito de goma. Explica el problema en voz alta, línea a línea, a un objeto inanimado. Suena ridículo y funciona: al obligarte a verbalizar lo que crees que hace el código, encuentras el punto donde lo que crees y lo que pasa no coinciden.
- Reduce a un ejemplo mínimo y divide. Saca el trozo sospechoso a un fichero nuevo de diez líneas con datos inventados: o el fallo desaparece —y la causa está en el contexto que quitaste— o se reproduce en diez líneas, que ya sabes depurar. Con
assertoprintlocaliza el punto exacto en que los datos dejan de ser lo que esperas; el fallo está entre el último punto correcto y el primero incorrecto. - Busca el mensaje exacto. Copia el texto del error sin tus nombres de variable:
TypeError: unsupported operand type(s) for -: 'str' and 'int', noerror en mi funcion resumen. La búsqueda del mensaje literal casi siempre da con el caso. - Lee la documentación oficial.
docs.python.orgtiene la respuesta a cualquier duda sobre la biblioteca estándar, con ejemplos. Un tutorial de un blog te da una receta; la documentación te da el modelo mental, y ese sirve para las siguientes veinte dudas. - Descansa. Después de cuarenta minutos atascado, la probabilidad de resolverlo cae en picado. Levantarse veinte minutos es la técnica de depuración más eficaz que existe y la más difícil de aplicar.
Sobre los asistentes de IA: son una herramienta legítima y estarán en tu vida profesional, pero en tu primer proyecto pueden robarte justo el aprendizaje que buscas. Sí a que expliquen un mensaje de error, a preguntar por qué una función se comporta como lo hace, a pedir una revisión de tu código ya escrito y a aclarar un concepto. No a «hazme el proyecto» ni a pegar código que no entiendes: un proyecto que no sabes explicar (09-04) no te sirve ni en una entrevista ni cuando haya que arreglarlo. Regla práctica: intenta el problema veinte minutos por tu cuenta antes de preguntar, y no pegues nunca código que no sabrías reescribir de memoria en lo esencial.
- Checklist de «terminado»
Antes de dar por cerrada la implementación y pasar a la presentación:
| # | Comprobación | ¿Hecho? |
|---|---|---|
| 1 | Todos los requisitos Must funcionan de punta a punta | |
| 2 | El guion de prueba manual pasa entero | |
| 3 | pytest en verde, con pruebas de modelo, lógica y persistencia |
|
| 4 | Primer arranque sin fichero de datos: funciona | |
| 5 | Ninguna entrada del usuario produce un traceback y los casos límite de 09-01 están comprobados | |
| 6 | black . y ruff check . sin quejas |
|
| 7 | Sin código muerto, print de depuración ni TODO olvidados |
|
| 8 | Todo commiteado; git status limpio |
|
| 9 | El programa arranca en una carpeta recién clonada |
La 9 es la que más falla y la que más vergüenza da en una demo: el programa funcionaba solo porque en tu carpeta había un fichero que no estaba en el repositorio. Pruébalo de verdad: clona en otro directorio y ejecuta.
Errores Comunes y Consejos
- Escribirlo todo antes de ejecutar nada. El error clásico. Treinta errores encadenados no se depuran, se abandonan. Ejecuta cada quince minutos.
- Dejar la validación para el final. «Ya pondré los
try/exceptcuando funcione» acaba con un programa que se rompe en la demo. La validación se escribe con la función, no después. - Perseguir el síntoma. Restar un número mágico para que el total cuadre tapa el fallo, que volverá con otros datos: arregla la causa y escribe la prueba de regresión.
- Abandonar las pruebas a mitad. Cuando aprietan las ganas de avanzar, lo primero que se cae son las pruebas, y justo entonces es cuando empiezan a hacer falta. Prueba al menos modelo y cálculos. Y no mezcles nunca refactorización con funcionalidad nueva en el mismo commit: si algo se rompe, no sabrás cuál de las dos fue.
- Consejo: termina cada sesión con el programa funcionando y commiteado, y deja un
SIGUIENTE.mdde dos líneas con lo próximo. Volver a un proyecto roto es la mejor forma de no volver, y esas dos líneas te ahorran veinte minutos de recontextualización en cada sesión.
Ejercicios
Son las siguientes tareas reales de tu proyecto. Al terminar, deberías tener la implementación cerrada.
Ejercicio 1: Esqueleto y modelo probado
Implementa la fase 1 (el programa arranca, muestra el menú y sale) y la fase 2 (tu entidad principal con validación completa y excepción propia). Escribe tests/test_modelo.py con al menos seis pruebas: un caso válido, la normalización de datos y cuatro casos inválidos, usando parametrize para los que compartan estructura. Haz un commit por fase.
Ejercicio 2: Persistencia con prueba de ida y vuelta
Implementa el almacén: guardar y cargar en el formato que decidiste en 09-02, tolerante a fichero ausente y corrupto. Escribe las tres pruebas con tmp_path: ciclo completo de ida y vuelta, fichero inexistente y fichero corrupto. Después conecta el almacén al esqueleto: al arrancar carga, al salir guarda.
Ejercicio 3: Un requisito de punta a punta y su guion
Elige tu requisito funcional principal (el equivalente a RF-1) e impleméntalo completo: opción del menú, entrada validada, llamada a la lógica, persistencia y confirmación en pantalla. Después escribe tu guion de prueba manual con un mínimo de diez comprobaciones que incluyan los casos límite de 09-01, y ejecútalo entero anotando lo que falle.
Soluciones
Solución 1. Los apartados 3 y 4 contienen la solución para MisGastos. Contrasta tu modelo con estos criterios: la validación está dentro de la entidad y no en la interfaz, la normalización ocurre antes que la validación, la excepción propia hereda de ValueError, y ninguna prueba usa una fecha fija que pueda caducar. Rúbrica: ¿tu modelo hace imposible construir un objeto inválido? ¿Tienes al menos una prueba por regla de validación? ¿pytest pasa en menos de un segundo?
Solución 2. Apartado 5. Los tres puntos que suelen fallar: que la ruta sea un parámetro con valor por defecto (sin eso no puedes usar tmp_path), que el fichero ausente devuelva vacío en vez de lanzar, y que los tipos sobrevivan al viaje —una fecha debe volver como date y un importe como float, no como cadenas—. Rúbrica: ¿la prueba de ida y vuelta comprueba valores concretos y no solo la longitud de la lista? ¿Las pruebas usan tmp_path y no tocan tu fichero real? ¿El primer arranque sin fichero funciona?
Solución 3. Apartado 7, con registrar_gasto y la tabla de diez comprobaciones. Lo esencial no es el código sino el reparto: la interfaz pide y muestra, el modelo valida, el cuaderno guarda en memoria, el almacén escribe. Si tu función de alta calcula algo del dominio o tu modelo imprime, la separación de capas se rompió. Rúbrica: ¿tu guion incluye el caso de lista vacía, el de entrada no numérica y el de identificador inexistente? ¿Pasa entero, sin un solo traceback? ¿Has hecho commit del requisito citando su número?
Conclusión
Implementar bien es, sobre todo, una cuestión de método. La estrategia es incremental y el programa está siempre ejecutable: primero el esqueleto que arranca y sale, después una funcionalidad completa de punta a punta y solo entonces la siguiente, girando cada quince o sesenta minutos el bucle escribir → ejecutar → probar → commit. El orden de construcción va de dentro hacia fuera —modelo con sus pruebas, almacén con su prueba de ida y vuelta, lógica de la colección, interfaz opción a opción y extras al final— porque las capas interiores son las que se prueban automáticamente y las que sostienen todo lo demás.
Las pruebas se escriben con el código, no después: un caso bueno, la normalización y todos los casos malos en el modelo, con parametrize para los que comparten estructura; el ciclo guardar/cargar y los ficheros ausente y corrupto en el almacén con tmp_path; los cálculos en la lógica, convirtiendo cada prueba de escritorio de 09-02 en un def test_; y para la interfaz, un guion manual escrito que se recorre entero antes de cada versión. Cuando algo falla, el método de 08-02 en seis pasos —reproducir, leer, formular una hipótesis concreta, comprobarla con el depurador, arreglar la causa y escribir la prueba de regresión— convierte un misterio en una tarea de veinte minutos. Los errores del usuario se reparten entre validar (lo corregible, en la interfaz) y capturar (lo imprevisible, con try/except), con las reglas del dominio siempre en el modelo. Los commits marcan cada incremento con un verbo y el requisito implementado, nunca en rojo y nunca mezclando refactorización con funcionalidad. Y para los bloqueos hay procedimiento: patito de goma, ejemplo mínimo, buscar el mensaje exacto, documentación oficial, dividir y probar, y descansar —con los asistentes de IA usados para entender y revisar, nunca para sustituirte, y siempre después de veinte minutos por tu cuenta—. La checklist de terminado, con su comprobación final de clonar en una carpeta limpia y arrancar, cierra la implementación.
Llegados aquí tienes un programa que funciona, está probado y vive en un repositorio ordenado. Falta lo que convierte un proyecto en algo que cuenta para los demás: saber enseñarlo. En Presentación del proyecto dejaremos el repositorio presentable, escribiremos el README como carta de presentación, prepararemos una demostración de cinco minutos y aprenderemos a defender las decisiones técnicas y a hablar de las limitaciones sin restarnos valor.
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
