Con la v0.18 el paquete tareafacil/ se entiende: tiene docstrings, anotaciones de tipo y un README. Pero sigue siendo frágil. Si Marta abre el tareas.json con un editor y se deja una coma de más, el programa muere con un traceback antes de mostrar el menú. Si Luis escribe «tres» cuando se le piden los días, muere igual. Y todo lo que hubiera en memoria se pierde. Esta lección cierra por fin la promesa que el curso arrastra desde Conversión de tipos y validación y desde Guardar datos en archivos: capturar los errores con try/except en lugar de rezar para que no ocurran. Aprenderás a distinguir los tres tipos de error, a leer un traceback sin miedo, a decidir qué se captura y qué se deja subir, a lanzar excepciones propias como TareaInvalida, a registrar lo que pasa con logging en vez de con print y a depurar con puntos de interrupción en lugar de adivinar. Al final, TareaFácil dejará de romperse.

Contenido

  1. Los tres tipos de error
  2. Leer un traceback
  3. Catálogo de excepciones habituales
  4. try/except: capturar lo que se debe capturar
  5. else y finally: el flujo completo
  6. raise y excepciones propias
  7. La política de errores: EAFP frente a LBYL
  8. logging frente a print
  9. Depurar de verdad: método, VS Code y pdb
  10. TareaFácil v0.19: el programa que no se rompe
  11. Errores comunes y consejos
  12. Ejercicios
  13. Conclusión

  1. Los tres tipos de error

No todos los fallos son iguales, y cada uno se caza de una manera distinta; distinguirlos es el primer paso para no perder el tiempo buscando donde no hay nada. El tercero es el peligroso: los dos primeros avisan a gritos, pero el error de lógica se queda callado, imprime «progreso: 0.6 %» y nadie se da cuenta hasta que Marta pregunta por qué la tarea del cartel lleva tres semanas al 0,6 %. Contra ese no hay try/except que valga: hacen falta el método de depuración del apartado 9 y las pruebas de Pruebas automatizadas.

Tipo Cuándo aparece Quién lo detecta Cómo se arregla
De sintaxis Antes de ejecutar nada Python, al leer el fichero Corrigiendo la escritura
De ejecución (excepción) Durante la ejecución El intérprete, al llegar a la línea Con validación o try/except
De lógica Nunca: el programa «funciona» Solo tú, mirando el resultado Depurando y probando
print("Tareas de Marta"          # 1. SyntaxError: no se ejecuta NADA del fichero
dias = int("tres")               # 2. ValueError: la sintaxis vale, el dato no
def progreso(hechos, dias):      # 3. Error de logica: nadie protesta...
    return hechos / dias         #    ...pero deberia ser hechos / dias * 100

  1. Leer un traceback

Cuando salta una excepción, Python imprime un traceback: el mapa de cómo se llegó hasta el fallo. Se lee de una forma muy concreta: de abajo arriba para saber qué pasó, y de arriba abajo para saber por qué.

Traceback (most recent call last):
  File "/home/marta/proyectos/tareafacil/__main__.py", line 34, in main
    agenda = almacen.cargar()
  File "/home/marta/proyectos/tareafacil/almacen.py", line 41, in cargar
    datos = json.load(f)
  File "/usr/lib/python3.12/json/__init__.py", line 293, in load
    return loads(f.read(), ...)
json.decoder.JSONDecodeError: Expecting ',' delimiter: line 8 column 3

Ese bloque contiene toda la información que necesitas:

  • most recent call last: las llamadas están en orden —arriba la primera, abajo la que ha estallado—; eso es la pila de llamadas. Y la última línea dice qué ha pasado: el tipo (JSONDecodeError) y el mensaje (falta una coma en la línea 8 del fichero de datos).
  • La línea culpable de tu código es la última que pertenece a tu proyecto, aquí almacen.py, line 41. Lo de debajo es la biblioteca estándar: json no falla, solo es donde se manifiesta.
  • El origen real suele estar más arriba: cargar() funciona bien; el problema es el fichero que se le ha dado. Recorre la pila hacia arriba preguntándote «¿quién le pasó este dato?»; en el 90 % de los casos el error está ahí.

  1. Catálogo de excepciones habituales

Estas son las que te vas a encontrar una y otra vez; reconocerlas de un vistazo ahorra muchísimo tiempo:

Excepción Causa típica
ValueError El tipo es correcto pero el valor no: int("tres")
TypeError Operación entre tipos incompatibles: "5" + 5
KeyError Clave que no existe en un diccionario: tarea["fecha"]
IndexError Índice fuera de rango: tareas[10] con 3 tareas
FileNotFoundError El fichero no existe o la ruta está mal
ZeroDivisionError División o módulo por cero
AttributeError El objeto no tiene ese atributo o método: None.titulo
json.JSONDecodeError El fichero JSON está corrupto o vacío

  1. try/except: capturar lo que se debe capturar

La sintaxis es simple: en el bloque try va el código que puede fallar; en el except, qué hacer si falla.

entrada = input("Dias estimados: ")
try:
    dias = int(entrada)                      # esta linea puede lanzar ValueError
except ValueError:
    print("Eso no es un numero entero. Uso 1 dia por defecto.")
    dias = 1                                 # y el programa continua

Sin el try, un «tres» tumbaba el programa. Con él, el usuario recibe un mensaje comprensible y la aplicación sigue viva. Ahora bien, hay dos formas de escribirlo mal:

except:                    # MAL: captura todo, incluso el Ctrl+C del usuario
    dias = 1
except Exception:          # CASI IGUAL DE MAL: oculta errores que no esperabas
    dias = 1

except: a secas captura hasta un KeyboardInterrupt (el Ctrl+C con el que el usuario intenta salir) y un SystemExit, así que tu programa se vuelve imposible de interrumpir. except Exception no llega tan lejos, pero sigue tapando errores de programación —un nombre mal escrito, un AttributeError— y convierte un fallo evidente en un comportamiento raro imposible de diagnosticar. La regla es: captura la excepción concreta que sabes que puede ocurrir y que sabes cómo manejar; lo demás debe subir y hacer ruido. Cuando hay varias posibilidades se encadenan except o se agrupan en una tupla, y con as se recoge el objeto para leer su mensaje:

try:
    with open(ruta, encoding="utf-8") as f:
        agenda = Agenda.from_dict(json.load(f))
except FileNotFoundError:
    agenda = Agenda()                                  # primera ejecucion
except (json.JSONDecodeError, KeyError) as error:      # dos casos, un bloque
    print(f"El fichero esta danado ({error}). Se usara una agenda vacia.")
    agenda = Agenda()

Los except se comprueban en orden y solo se ejecuta el primero que encaje: por eso las excepciones concretas van siempre antes que las generales.

  1. else y finally: el flujo completo

El bloque try admite dos cláusulas más que redondean la estructura. else se ejecuta solo si no hubo excepción, lo que permite dejar en el try únicamente la línea peligrosa; finally se ejecuta siempre, haya fallado o no, incluso si el try hace return, y es el sitio de la limpieza.

def cargar_json(ruta):
    """Devuelve los datos del fichero, o None si no se pudo leer."""
    try:
        with open(ruta, encoding="utf-8") as f:
            datos = json.load(f)
    except (FileNotFoundError, json.JSONDecodeError) as error:
        print(f"No se pudo leer {ruta}: {error}")
        return None
    else:
        return datos                        # solo si todo fue bien
    finally:
        print("Fin del intento de carga.")  # pase lo que pase
flowchart TD
    A[Entra en try] --> B{Hay excepcion?}
    B -- No --> C[Ejecuta else] --> G[Ejecuta finally]
    B -- Si --> D{Coincide algun except?}
    D -- Si --> E[Ejecuta ese except] --> G
    D -- No --> F[La excepcion sube al que llamo] --> G
    G --> H[Sigue el programa o propaga el error]

Fíjate en la rama de la derecha: si ningún except encaja, el finally se ejecuta igualmente y después la excepción continúa subiendo. Esa es la clave del diseño: finally garantiza la limpieza sin tragarse el error. Para los ficheros, eso ya lo resuelve algo que usas desde 05-05: with cierra el fichero automáticamente y hace innecesario el finally en ese caso.

  1. raise y excepciones propias

Hasta ahora las excepciones venían de Python. También puedes lanzarlas tú, y es lo correcto cuando tu función recibe algo con lo que no puede trabajar: fallar pronto y con un mensaje claro es mejor que devolver un valor inventado.

def crear_tarea(titulo, dias):
    """Crea una tarea validando los datos de entrada."""
    if not titulo.strip():
        raise ValueError("El titulo no puede estar vacio.")
    if dias <= 0:
        raise ValueError(f"Los dias deben ser positivos, y llegaron {dias}.")
    return Tarea(titulo, dias)

try:
    tarea = crear_tarea("", 3)
except ValueError:
    print("Aviso: datos invalidos.")
    raise                      # 'raise' a secas relanza la misma excepcion

Un raise a secas dentro de un except relanza la excepción original con su traceback intacto, que es lo que quieres cuando solo necesitas anotar algo de paso. Y cuando conviertes un error de bajo nivel en otro más significativo, encadénalos con raise ... from: raise TareaInvalida("prioridad desconocida") from error conserva la causa en el traceback. Definir una excepción propia es sorprendentemente simple, una clase vacía que hereda de otra excepción:

class TareaInvalida(ValueError):
    """Los datos de una tarea no cumplen las reglas del Estudio Alba."""

class PrioridadInvalida(TareaInvalida):     # se pueden encadenar en jerarquia
    """La prioridad no es 'alta', 'media' ni 'baja'."""

¿Por qué heredar de ValueError y no de Exception? Porque así el código antiguo sigue funcionando: quien capturaba ValueError para validar entradas seguirá capturando también las tuyas. Recuerda además que todas las excepciones descienden de Exception, y que esa jerarquía manda: except ValueError no atrapa un KeyError, pero except Exception los atrapa todos, incluidos los que no esperabas. Una jerarquía propia da precisión al capturar (except TareaInvalida atrapa cualquier problema de datos de tarea, incluida PrioridadInvalida, sin tocar un ValueError de otra parte), mensajes del dominio en lugar de mensajes sobre conversiones de texto, y un único sitio que documenta las reglas del negocio. La convención es agruparlas en el módulo del dominio —en TareaFácil, modelo.py— con nombres reconocibles (...Invalida, ...Error).

  1. La política de errores: EAFP frente a LBYL

Toda aplicación necesita una política: qué se captura y qué se deja subir. La regla es fácil de recordar: captura un error solo si puedes hacer algo útil con él —pedir el dato otra vez, usar un valor por defecto, avisar al usuario—. Si no puedes, déjalo subir: un fallo visible es infinitamente mejor que uno silencioso. Para validar hay además dos estilos, y Python tiene preferencia por uno:

if ruta.exists():                  # LBYL (Look Before You Leap), como en 05-05
    with open(ruta, encoding="utf-8") as f:
        datos = json.load(f)
else:
    datos = []

try:                               # EAFP (Easier to Ask Forgiveness than Permission)
    with open(ruta, encoding="utf-8") as f:
        datos = json.load(f)
except FileNotFoundError:          # el estilo que prefiere Python
    datos = []
Estilo Ventaja Inconveniente
LBYL Se lee como una condición normal Deja una rendija: el fichero puede borrarse entre la comprobación y la apertura
EAFP No hay rendija: o se abre o se captura Requiere conocer la excepción concreta

Python prefiere EAFP, y con ficheros es objetivamente más seguro: entre el exists() y el open() pasa un instante en el que otro programa puede borrar o mover el fichero. Aquella comprobación con Path.exists() de 05-05 era correcta para lo que sabíamos entonces; esta es la versión robusta que prometimos.

  1. logging frente a print

Cuando algo falla, la tentación es sembrar el código de print("aqui llego"). Funciona para salir del paso, pero en un programa real es un mal sistema: hay que borrarlos después (y siempre se olvida alguno), se mezclan con la salida legítima y no dejan constancia de nada. El módulo logging de la biblioteca estándar resuelve las tres cosas.

Nivel Cuándo usarlo Ejemplo en TareaFácil
DEBUG Detalle interno útil al programar «Cargadas 12 tareas de tareas.json»
INFO Acontecimientos normales «Tarea creada: Cartel feria del libro»
WARNING Algo raro, pero se puede seguir «El fichero no existe; agenda vacía»
ERROR / CRITICAL Una operación falló / no se puede continuar «No se pudo guardar el JSON»
import logging

logging.basicConfig(
    filename="tareafacil.log",              # sin esto, sale por pantalla
    level=logging.INFO,                     # se registran INFO y superiores
    format="%(asctime)s %(levelname)s %(message)s",
)
try:
    guardar(agenda)
except OSError:
    logging.exception("Fallo al guardar")   # incluye el traceback completo

Tres detalles marcan la diferencia. level decide qué se registra: subiéndolo a WARNING silencias los mensajes de detalle sin tocar una línea de código. filename manda todo a un fichero, así que mañana puedes ver qué ocurrió sin haber estado delante. Y logging.exception(), usado dentro de un except, escribe el mensaje y el traceback entero: es la forma correcta de registrar un fallo que has capturado y no quieres perder.

  1. Depurar de verdad: método, VS Code y pdb

Depurar no es mirar el código fijamente hasta que confiese. Es un método de cuatro pasos:

  1. Reproducir: encontrar la secuencia exacta que provoca el fallo, siempre. Un error que no sabes reproducir no lo puedes arreglar.
  2. Aislar: reducir el caso al mínimo. ¿Falla con una tarea o hacen falta veinte? ¿Con cualquier prioridad?
  3. Formular una hipótesis concreta («creo que dias llega como texto desde el JSON») y comprobarla: una sola cosa cada vez; si es falsa, se descarta y se formula otra.

Para ese último paso está el depurador. En VS Code haces clic a la izquierda del número de línea para poner un punto de interrupción (un círculo rojo), arrancas con F5 y el programa se detiene justo antes de ejecutar esa línea; ahí puedes inspeccionar las variables en el panel lateral, ver la pila de llamadas y avanzar con F10 (ejecuta la línea entera), F11 (entra en la función) y F5 (continúa). Sin editor gráfico, escribe breakpoint() en la línea que te interese y ejecuta: se abrirá una consola (Pdb) en ese punto.

Comando Qué hace
l (list) Muestra el código alrededor de la línea actual
n (next) Ejecuta la línea y pasa a la siguiente
s (step) Entra dentro de la función que se llama
c (continue) Sigue hasta el siguiente breakpoint()
p expresion Imprime el valor de una variable o expresión
w (where) / q (quit) Muestra la pila de llamadas / sale del depurador

Y una última herramienta, en dos líneas: assert condicion, "mensaje" lanza un AssertionError si la condición es falsa. Sirve para comprobar suposiciones internas mientras desarrollas, pero no para validar datos del usuario, porque Python puede desactivar los assert con la opción -O. Su sitio natural son las pruebas, y ahí lo retomamos en 08-04.

  1. TareaFácil v0.19: el programa que no se rompe

Primero, almacen.py sobrevive a que el JSON no exista o esté corrupto:

"""Persistencia de TareaFacil: JSON y exportacion a CSV."""
RUTA_JSON = Path("tareas.json")

def cargar_tareas(ruta: Path = RUTA_JSON) -> Agenda:
    """Devuelve la agenda guardada; una agenda vacia si no se pudo leer."""
    try:
        with open(ruta, encoding="utf-8") as f:
            return Agenda.from_dict(json.load(f))
    except FileNotFoundError:
        logging.info("No existe %s; se empieza con una agenda vacia.", ruta)
    except json.JSONDecodeError as error:
        logging.error("El fichero %s esta danado: %s", ruta, error)
        ruta.replace(ruta.with_suffix(".json.bak"))   # no se pierde: se aparta
    return Agenda()

Fíjate en la decisión de diseño: ante un fichero corrupto no se borra nada, se aparta con otro nombre y se sigue, así que el usuario no pierde sus datos. Segundo, el modelo valida y lanza su propia excepción, y la interfaz la captura para volver a preguntar:

class Tarea:
    def __init__(self, titulo: str, responsable: str, prioridad: str = "media",
                 dias: int = 1) -> None:
        if prioridad.strip().lower() not in PRIORIDADES:
            raise TareaInvalida(f"Prioridad no valida: {prioridad!r}")
        try:
            self.dias = int(dias)
        except (ValueError, TypeError) as error:
            raise TareaInvalida(f"Dias no valido: {dias!r}") from error

def pedir_prioridad() -> str:                 # en tareafacil/interfaz.py
    """Pide una prioridad hasta que sea valida."""
    while True:
        try:
            return Tarea("temporal", "marta", input("Prioridad: ")).prioridad
        except TareaInvalida as error:
            print(f"  {error} Intentalo de nuevo.")

Y en __main__.py, la configuración del registro y una red de seguridad final para que ningún fallo inesperado se lleve por delante los datos:

def main() -> None:
    """Ejecuta TareaFacil registrando los acontecimientos en tareafacil.log."""
    logging.basicConfig(filename="tareafacil.log", level=logging.INFO,
                        format="%(asctime)s %(levelname)s %(message)s")
    agenda = almacen.cargar_tareas()
    try:
        bucle_principal(agenda)
    except KeyboardInterrupt:                 # el usuario ha pulsado Ctrl+C
        print("\nSaliendo...")
    finally:
        almacen.guardar(agenda)               # pase lo que pase, se guarda

TareaFácil v0.19: un JSON corrupto ya no tumba el programa, una prioridad inventada se rechaza con un mensaje del dominio, un dias de texto se convierte o se rechaza con claridad, un Ctrl+C guarda antes de salir y todo queda registrado en tareafacil.log.

Errores Comunes y Consejos

  • Usar except: o except Exception por comodidad, o capturar y no hacer nada (except ValueError: pass). Ocultan errores de programación y crean fallos invisibles: captura la excepción concreta, y si no puedes hacer nada útil con ella, déjala subir.
  • Poner el except general antes que los concretos, o meter medio programa dentro del try. Se comprueban en orden, así que el primero que encaje gana; y en el try debe ir solo la línea que puede fallar, con el resto en el else.
  • Leer el traceback por encima, o depurar con print en un programa real. La última línea dice qué pasó y la última línea tuya dice dónde; y para lo demás está logging, que se filtra por nivel, va a un fichero y no hay que borrarlo después.
  • Consejo: escribe el mensaje pensando en quien lo va a leer. «Prioridad no valida: 'urgente'. Usa alta, media o baja» vale mil veces más que «error de validación».

Ejercicios

Ejercicio 1: Leer un traceback

Explica qué ha pasado, en qué línea de código propio está el problema y cómo lo arreglarías:

Traceback (most recent call last):
  File "informes.py", line 20, in <module>
    print(media_dias(tareas))
  File "informes.py", line 15, in media_dias
    return sum(t["dias"] for t in tareas) / len(tareas)
ZeroDivisionError: division by zero

Ejercicio 2: De frágil a robusto

Reescribe esta función para que sobreviva a un fichero inexistente, a una línea mal formada y a un valor no numérico, registrando cada problema con logging y devolviendo lo que sí se haya podido leer:

def leer_horas(ruta):
    horas = []
    with open(ruta, encoding="utf-8") as f:
        for linea in f:
            nombre, valor = linea.split(",")
            horas.append((nombre, int(valor)))
    return horas

Ejercicio 3: Excepción propia

Define una excepción PresupuestoInvalido para el Estudio Alba y una función validar_presupuesto(importe, cliente) que la lance si el importe no es un número, si es negativo o si supera los 50.000 € (límite que exige la aprobación de Marta). Escribe después el try/except que la usa mostrando un mensaje distinto en cada caso.

Soluciones

Solución 1.

La última línea dice qué: una división por cero. La última línea de código propio dice dónde: informes.py, line 15, en media_dias. Como sum(...) no divide, el cero solo puede ser len(tareas): la lista de tareas está vacía. El error real, sin embargo, no está en la línea 15 sino en quien llamó desde la línea 20 con una lista vacía; la función solo lo ha manifestado.

def media_dias(tareas: list[dict]) -> float:
    """Devuelve la media de dias estimados; 0.0 si no hay tareas."""
    if not tareas:                       # LBYL: caso previsto y legitimo
        return 0.0
    return sum(t["dias"] for t in tareas) / len(tareas)

Si una lista vacía fuera un caso imposible —un error de programación en quien llama—, lo correcto sería lo contrario: no capturarlo y dejar que el ZeroDivisionError haga ruido.

Solución 2.

def leer_horas(ruta: str) -> list[tuple[str, int]]:
    """Devuelve las horas leidas del fichero, saltando las lineas invalidas."""
    horas: list[tuple[str, int]] = []
    try:
        lineas = Path(ruta).read_text(encoding="utf-8").splitlines()
    except FileNotFoundError:
        logging.error("No existe el fichero de horas: %s", ruta)
        return horas                       # lista vacia: el programa sigue
    for numero, linea in enumerate(lineas, start=1):
        try:
            nombre, valor = linea.strip().split(",")
            horas.append((nombre.strip().lower(), int(valor)))
        except ValueError as error:        # cubre el split y el int
            logging.warning("Linea %d ignorada: %s", numero, error)
    return horas

Dos ideas importantes. El try interior envuelve solo las dos líneas que pueden fallar, y captura ValueError porque tanto un split que no devuelve dos partes como un int("ocho") lanzan esa misma excepción. Y el fallo de una línea no aborta el fichero entero: se registra con su número y el bucle continúa. Esa es la diferencia entre un programa frágil y uno robusto: sigue haciendo todo lo que sí puede hacer.

Solución 3.

LIMITE_APROBACION = 50000       # a partir de aqui, lo aprueba Marta

class PresupuestoInvalido(ValueError):
    """El importe de un presupuesto no cumple las reglas del Estudio Alba."""

def validar_presupuesto(importe, cliente: str) -> float:
    """Devuelve el importe validado o lanza PresupuestoInvalido."""
    try:
        importe = float(importe)
    except (TypeError, ValueError) as error:
        raise PresupuestoInvalido(f"Importe no numerico: {importe!r}") from error
    if importe < 0:
        raise PresupuestoInvalido(f"Importe negativo para {cliente}: {importe}")
    if importe > LIMITE_APROBACION:
        raise PresupuestoInvalido(f"{importe} EUR supera el limite; lo aprueba Marta.")
    return importe

try:
    total = validar_presupuesto("12500", "Panaderia Sole")
except PresupuestoInvalido as error:
    print(f"Presupuesto rechazado: {error}")
else:                                    # solo si no hubo excepcion
    print(f"Presupuesto aceptado: {total:.2f} EUR")

Fíjate en el from error de la primera conversión: conserva la causa original en el traceback, de modo que quien depure sabrá que detrás de PresupuestoInvalido había un ValueError de float(). Los tres mensajes son distintos y dicen qué hacer, que es la marca de un buen error. Y el else deja claro que la línea de éxito solo se ejecuta si no hubo excepción.

Conclusión

Los errores son de tres tipos: los de sintaxis impiden ejecutar, los de ejecución lanzan excepciones y los de lógica no avisan de nada y solo se cazan depurando y probando. Ante una excepción, el traceback lo cuenta todo: la última línea dice qué ha pasado, la última línea de código propio dice dónde, y el origen real suele estar más arriba en la pila. Reconocer el catálogo habitual —ValueError, TypeError, KeyError, IndexError, FileNotFoundError, ZeroDivisionError, AttributeError, json.JSONDecodeError— ahorra la mitad del trabajo. Con try/except se captura la excepción concreta que sabes manejar, nunca except: a secas ni except Exception, colocando los casos concretos antes que los generales, agrupando varios en una tupla y recogiendo el objeto con as error; else ejecuta lo que solo tiene sentido si no hubo fallo y finally garantiza la limpieza pase lo que pase, incluso cuando la excepción sigue subiendo. Con raise lanzas errores a propósito para fallar pronto y con un mensaje claro, raise a secas relanza sin perder el traceback y raise ... from conserva la causa. Una excepción propia como TareaInvalida, heredada de ValueError, aporta precisión al capturar y mensajes del dominio. La política se resume en una frase: captura solo lo que puedas resolver y deja subir el resto, prefiriendo EAFP a LBYL cuando hay ficheros de por medio. Para saber qué está pasando, logging sustituye al print con sus cinco niveles, su salida a fichero y su logging.exception(). Y para los errores de lógica, el método —reproducir, aislar, formular hipótesis, comprobar— junto con los puntos de interrupción de VS Code y breakpoint()/pdb.

TareaFácil es ahora la v0.19: un tareas.json corrupto se aparta con una copia en lugar de tumbar el programa, una prioridad inventada se rechaza con TareaInvalida y la interfaz vuelve a preguntar, un dias que llega como texto se convierte o se explica, un Ctrl+C guarda antes de salir y todo queda anotado en tareafacil.log. El programa aguanta. Pero fíjate en lo que acabas de hacer: has cambiado almacen.py, modelo.py, interfaz.py y __main__.py a la vez, y no hay forma de volver atrás si algo de esto ha empeorado otra cosa. No existe una copia del estado anterior, ni un registro de qué se tocó ni por qué. Eso se acaba en Control de versiones: Git, el historial del proyecto, y la tranquilidad de poder experimentar sabiendo que nada se pierde.

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