En la lección anterior aprendimos a leer el aviso; en esta aprendemos a hacernos cargo de él. La herramienta es la estructura try/except, y con ella Papyrus va a saldar hoy tres deudas concretas: cargar_catalogo() dejará de morir cuando catalogo.json no exista (adiós al paliativo Path.exists()), el peaje str→float del CSV dejará de "rezar para que traiga números" y sobrevivirá a filas corruptas, y el int(input()) que arrastra su ValueError desde el módulo 1 — parcheado en el módulo 2 con isdigit() — recibirá por fin su validación definitiva. Por el camino veremos las cuatro piezas de la estructura completa (try, except, else, finally), cómo capturar el objeto excepción con as e, y una tabla de antipatrones encabezada por el peor de todos: el except: pelado.
Contenido
- La estructura
try/except: pedir perdón, por fin - Capturar tipos concretos (y por qué nunca
except:pelado) - Varios
excepty capturar el objeto conas e - Las piezas restantes:
elseyfinally - Papyrus, versión robusta:
cargar_catalogo()y el CSV de ventas - Saldar la deuda del módulo 1:
pedir_entero()
La estructura try/except: pedir perdón, por fin
La sintaxis mínima tiene dos bloques: lo que intentas y lo que haces si falla:
try:
precio = float(texto) # el intento
except ValueError:
print(f"'{texto}' no es un precio válido.") # el plan B
precio = 0.0Cómo fluye la ejecución, paso a paso:
- Python ejecuta el bloque
trylínea a línea. - Si no salta ninguna excepción, el bloque
exceptse ignora por completo y la vida sigue. - Si una línea del
trylanza una excepción, la ejecución abandona eltryinmediatamente (las líneas restantes del bloque no se ejecutan) y Python busca unexceptcuyo tipo coincida. - Coincidir significa: la excepción es de esa clase o de una subclase — la jerarquía de 07-01 en acción. Un
except LookupError:atrapa tantoKeyErrorcomoIndexError. - Si ningún
exceptcoincide, la excepción sigue propagándose hacia arriba, como si eltryno existiera.
El punto 5 es importante: try no es un escudo mágico. Solo atrapa lo que le dices que atrape. Y eso es una virtud, como vamos a ver ahora mismo.
Capturar tipos concretos (y por qué nunca except: pelado)
La tentación del principiante es "que no falle nada": envolverlo todo y atraparlo todo. Es el error más caro de todo el módulo, porque convierte errores ruidosos en errores silenciosos — y un error silencioso puede costarle dinero a Ana sin que nadie se entere.
# ❌ EL ANTIPATRÓN: nunca hagas esto
try:
catalogo = cargar_catalogo()
except: # atrapa TODO, incluso lo que no debería
catalogo = {} # ...y lo esconde debajo de la alfombra¿Qué esconde ese except: pelado? Todo. Si hay una errata catalgo dentro de cargar_catalogo() (un NameError, un bug tuyo), el programa arranca con catálogo vacío y nadie sabrá jamás por qué. Si Ana pulsa Ctrl+C para cancelar (KeyboardInterrupt, que hereda de BaseException y también cae en un except: pelado), el programa lo ignora. El traceback — ese informe forense gratuito de 07-01 — va directo a la basura.
| Antipatrón | Por qué es dañino | Alternativa correcta |
|---|---|---|
except: pelado |
Atrapa hasta KeyboardInterrupt y SystemExit; oculta bugs propios |
except TipoConcreto: |
except Exception: como reflejo |
Menos grave (deja pasar Ctrl+C) pero sigue mezclando bugs con fallos esperables | Capturar el tipo que sabes manejar; Exception solo en la frontera del programa (07-05) |
except ValueError: pass |
Silencia el error sin dejar rastro alguno | Como mínimo, avisar o contar; idealmente registrar en el log (07-05) |
try gigante alrededor de 30 líneas |
Imposible saber qué línea falló ni si el except maneja lo correcto |
try mínimo: solo la(s) línea(s) que pueden fallar de esa forma |
Atrapar y devolver un valor "normal" (return -1) |
El error se disfraza de dato y explota más lejos, donde es ilegible | Devolver None con criterio (07-03) o dejar propagar |
La regla que resume la tabla: captura solo lo que esperas y sabes manejar; deja que todo lo demás explote. Una excepción no manejada con su traceback es infinitamente mejor que un programa que miente.
Varios except y capturar el objeto con as e
Un mismo try puede fallar de formas distintas que merecen reacciones distintas. Se encadenan varios except, y Python usa el primero que coincida (por eso los tipos específicos van antes que los genéricos):
try:
with open(ruta, encoding="utf-8") as f:
datos = json.load(f)
except FileNotFoundError:
print("No hay catálogo previo. Se empezará con uno vacío.")
datos = []
except json.JSONDecodeError as e:
print(f"El catálogo está corrupto ({e}). Revisa el archivo o restaura una copia.")
raise SystemExit(1)Dos novedades aquí:
as eguarda el objeto excepción en la variablee, disponible dentro del bloque. Recuerda de 07-01 que la excepción es un objeto:print(e)muestra su mensaje, y algunos tipos llevan atributos extra (e.linenoenJSONDecodeErrordice en qué línea del JSON está el destrozo). La variable solo existe dentro de su bloqueexcept.json.JSONDecodeErrores la respuesta a otra pregunta del cierre de M6: "¿y si la copia está corrupta?". Si alguien editó malcatalogo.jsona mano — el módulo 6 solo pudo suplicar "no edites mal el JSON" —,json.load()lanza esta excepción, y ahora sabemos distinguir archivo ausente (recuperable: catálogo vacío) de archivo corrupto (no inventes datos: avisa y detente).
Si varias excepciones merecen la misma reacción, se agrupan en una tupla: except (ValueError, KeyError) as e:.
Las piezas restantes: else y finally
La estructura completa tiene cuatro bloques, cada uno con un papel preciso:
try:
f = open(ruta, encoding="utf-8") # puede fallar
except FileNotFoundError:
print("Archivo no encontrado.") # plan B
else:
contenido = f.read() # solo si el try FUE BIEN
f.close()
finally:
print("Fin del intento de lectura.") # SIEMPRE, pase lo que pase| Bloque | Se ejecuta cuando... | Para qué sirve |
|---|---|---|
try |
Siempre (es el intento) | La(s) operación(es) que pueden fallar |
except |
Solo si el try lanzó una excepción coincidente |
El plan B: recuperarse, avisar, registrar |
else |
Solo si el try terminó sin excepción |
El trabajo que depende del éxito, fuera del try para no atrapar sus errores por accidente |
finally |
Siempre: con éxito, con excepción manejada, con excepción no manejada e incluso con return por medio |
Limpieza garantizada: cerrar, liberar, dejar todo en orden |
¿Por qué molestarse con else en vez de escribirlo todo dentro del try? Precisión: si f.read() estuviera dentro del try y fallara por otro motivo, un except demasiado amplio podría atraparlo y diagnosticar mal. El try debe abrigar solo las líneas cuyo fallo sabes manejar; else recoge la continuación del camino feliz.
¿Y finally? Te sonará la garantía: es exactamente lo que el with del módulo 6 hace por ti con los archivos — cerrar pase lo que pase. De hecho, with está construido sobre esta mecánica, y en el módulo 8 aprenderemos a fabricar nuestros propios context managers. Mientras tanto, la pareja práctica es: with para archivos, finally para cualquier otra limpieza que deba ocurrir sí o sí (imprimir un resumen de cierre, restaurar un estado).
flowchart TD
A[try] -->|sin excepción| B[else]
A -->|excepción coincidente| C[except]
A -->|excepción NO coincidente| D[se propaga hacia arriba]
B --> E[finally]
C --> E
D --> E
E -->|si venía propagándose| F[sigue propagándose]
Papyrus, versión robusta: cargar_catalogo() y el CSV de ventas
Hora de pagar las deudas del módulo 6. Así quedó cargar_catalogo() entonces, paliativo incluido:
# Versión M6 — con paliativo
def cargar_catalogo():
ruta = BASE / "datos" / "catalogo.json"
if not ruta.exists(): # LBYL: foto del pasado
return {}
with open(ruta, encoding="utf-8") as f:
lista = json.load(f)
return {l["titulo"].strip().casefold(): Libro(**l) for l in lista}Y así queda la versión EAFP, que además distingue ausente de corrupto:
# Versión M7 — EAFP
def cargar_catalogo():
ruta = BASE / "datos" / "catalogo.json"
try:
with open(ruta, encoding="utf-8") as f:
lista = json.load(f)
except FileNotFoundError:
print("Aviso: no existe catalogo.json. Se parte de un catálogo vacío.")
return {}
except json.JSONDecodeError as e:
print(f"Error: catalogo.json está corrupto ({e}).")
print("Restaura una copia de copias/ antes de continuar.")
raise SystemExit(1)
return {l["titulo"].strip().casefold(): Libro(**l) for l in lista}Punto por punto:
- El
trycubre solo la apertura y el parseo — las dos operaciones que pueden fallar de estas formas. La construcción del diccionario queda fuera: siLibro(**l)fallara (un precio negativo en el JSON dispararía elValueErrordel__post_init__de M5), queremos ver ese traceback, no confundirlo con un problema de archivo. FileNotFoundErrores un caso esperado y recuperable: primer arranque en un ordenador nuevo. Catálogo vacío + aviso honesto. Sin condición de carrera: elopen()es la comprobación.JSONDecodeErrores esperado pero no recuperable en silencio: inventar un catálogo vacío cuando existe uno corrupto haría que Ana machacara sus datos reales al guardar. Avisamos con el detalle del error y paramos. Manejar bien una excepción a veces significa detenerse mejor, no continuar a cualquier precio.
Segunda deuda: el peaje str→float de ventas.csv (06-02). Si una fila trae "gratis" como importe, la versión M6 muere en plena suma. La M7 salta la fila corrupta, cuenta los errores y sigue:
def cierre_de_caja():
ruta = BASE / "datos" / "ventas.csv"
total, ventas_ok, filas_corruptas = 0.0, 0, 0
try:
with open(ruta, encoding="utf-8", newline="") as f:
for fila in csv.DictReader(f):
try:
total += float(fila["importe"]) # el peaje str→float
except ValueError:
filas_corruptas += 1
print(f" Fila ignorada, importe ilegible: {fila!r}")
else:
ventas_ok += 1
except FileNotFoundError:
print("Sin ventas registradas hoy.")
return 0.0
if filas_corruptas:
print(f"Atención: {filas_corruptas} fila(s) corrupta(s) en ventas.csv.")
print(f"Cierre de caja: {ventas_ok} ventas, total {total:.2f} €")
return totalObserva el try anidado y mínimo: el interior abriga una sola línea (la conversión) y atrapa un solo tipo (ValueError). Una fila mala ya no tumba el cierre del día — pero tampoco desaparece: se cuenta y se muestra, porque silenciarla sería el antipatrón pass de la tabla. El else interior suma la venta al contador solo cuando la conversión fue bien: exactamente el papel que le asignamos a else.
Saldar la deuda del módulo 1: pedir_entero()
La deuda más antigua del curso. En el módulo 1, int(input()) explotaba si Júlia tecleaba "tres". En el módulo 2 lo parcheamos con while True + isdigit(), y ya entonces avisamos de que era provisional. ¿Por qué? Porque isdigit() intenta predecir lo que int() aceptará, y predice mal: "-2".isdigit() es False (rechaza negativos legítimos), " 4 ".isdigit() es False (rechaza espacios que int() tolera). Es LBYL con un validador que no coincide con el conversor. La solución EAFP: que decida el propio int().
def pedir_entero(mensaje):
"""Pide un entero por teclado hasta que la entrada sea válida."""
while True:
texto = input(mensaje)
try:
return int(texto) # si convierte, devolvemos y salimos
except ValueError:
print(f"'{texto}' no es un número entero. Inténtalo de nuevo.")
unidades = pedir_entero("¿Cuántos ejemplares de Hamlet? ")- El
returndentro deltryrompe el bucle en cuanto hay un entero válido — no hace faltabreak. - Si
int()lanzaValueError, elexceptinforma (mostrando lo que se tecleó, mensaje accionable) y elwhile Truereintenta. - El juez es
int()mismo: acepta"-2"y" 4 ", rechaza"tres"y"4.5". Cero desacuerdos entre validador y conversor, porque son la misma cosa.
La misma plantilla sirve para pedir_precio() con float(): guárdala, porque es de las funciones más reutilizadas que escribirás.
Errores Comunes y Consejos
except:pelado. El antipatrón número uno. Atrapa hasta el Ctrl+C de Ana y entierra tus propios bugs. Siempre un tipo concreto.trykilométrico. Si el bloque tiene diez líneas, ya no sabes cuál falló ni si tuexceptes la reacción adecuada. Encoge eltryhasta la línea culpable; usaelsepara la continuación.- Silenciar con
pass.except ValueError: passhace desaparecer datos sin dejar rastro. Encierre_de_caja()las filas corruptas se cuentan y se muestran; en 07-05 irán al log. - Ordenar mal los
except. Python usa el primero que coincide: si ponesexcept Exception:antes queexcept ValueError:, el segundo es código muerto. De lo específico a lo general. - Olvidar que
elseexiste. No es obligatorio, pero meter el camino feliz dentro deltry"porque cabe" acaba atrapando excepciones que no querías atrapar. - Consejo: antes de escribir un
except, pregúntate: "¿qué voy a hacer con este error?". Si la respuesta es "nada", probablemente no debas atraparlo: deja que se propague hacia quien sí pueda hacer algo. - Consejo: fíjate en que nuestros mensajes de aviso siempre incluyen el dato culpable (
'{texto}',{fila!r}). Un aviso sin el dato obliga a adivinar; con el dato, se arregla en un minuto.
Ejercicios
-
pedir_precio(). Escribe la gemela depedir_entero(): pide un precio confloat(), reintenta anteValueErrory, además, rechaza precios negativos con un mensaje distinto ("un precio no puede ser negativo") sin salir del bucle. Pruébala con"9,95"(coma decimal, muy típico en España): ¿qué ocurre y por qué? -
cargar_config()a prueba de todo. El módulo 6 dejóconfig.jsoncon{"nombre_tienda", "iva_libros", "descuento_socio"}. Escribecargar_config()que: devuelva el diccionario si todo va bien; anteFileNotFoundErrordevuelva la configuración por defecto{"nombre_tienda": "Papyrus", "iva_libros": 0.04, "descuento_socio": 0.05}con un aviso; y antejson.JSONDecodeErrormuestre el error y relanceSystemExit(1). Añade unfinallyque imprima siempre"Configuración: proceso terminado."y comprueba que aparece en los tres escenarios. -
Cazar el antipatrón. Este código "funciona", pero comete al menos tres pecados de la tabla de antipatrones. Identifícalos y reescríbelo:
def precio_socio(catalogo, titulo): try: libro = catalogo[titulo.strip().casefold()] precio = libro.precio * (1 - DESCUENTO_SOCIO) print("El precio para socios de " + titulo + " es " + str(round(precio, 2))) return precio except: return -1
Soluciones
-
def pedir_precio(mensaje): while True: texto = input(mensaje) try: precio = float(texto) except ValueError: print(f"'{texto}' no es un número. Usa punto decimal, p. ej. 9.95") continue if precio < 0: print("Un precio no puede ser negativo. Inténtalo de nuevo.") continue return precioCon
"9,95"saltaValueError:float()solo entiende el punto decimal. Por eso el mensaje delexceptsugiere el formato correcto — mensaje accionable. (Alternativa amable:texto.replace(",", ".")antes de convertir.) Nota el orden: primero la conversión (EAFP), luego la validación del valor ya convertido (LBYL sobre un dato que ya tienes en la mano, legítimo según vimos en 07-01). -
CONFIG_DEFECTO = {"nombre_tienda": "Papyrus", "iva_libros": 0.04, "descuento_socio": 0.05} def cargar_config(): ruta = BASE / "datos" / "config.json" try: with open(ruta, encoding="utf-8") as f: return json.load(f) except FileNotFoundError: print("Aviso: sin config.json; se usan valores por defecto.") return dict(CONFIG_DEFECTO) except json.JSONDecodeError as e: print(f"config.json corrupto: {e}") raise SystemExit(1) finally: print("Configuración: proceso terminado.")El
finallyse imprime en los tres escenarios — incluso con losreturnde por medio e incluso cuandoSystemExitestá propagándose. Esa es su garantía. Detalle fino: se devuelvedict(CONFIG_DEFECTO)(una copia) para que nadie mute el diccionario por defecto compartido, recuerdo del aliasing del módulo 4. -
Pecados: (1)
except:pelado — atraparía hasta unNameErrorsiDESCUENTO_SOCIOno estuviera definido, ocultando un bug; (2)trydemasiado ancho — solo el accesocatalogo[...]puede fallar de forma esperable (KeyError); elprinty la aritmética no deberían estar abrigados; (3) devolver-1como error disfrazado de precio — quien reciba ese-1podría cobrárselo a Júlia. Reescritura:def precio_socio(catalogo, titulo): try: libro = catalogo[titulo.strip().casefold()] except KeyError: print(f"'{titulo}' no está en el catálogo.") return None return round(libro.precio * (1 - DESCUENTO_SOCIO), 2)¿Y es
Nonela mejor señal de "no encontrado"? ¿O debería esta función lanzar su propia excepción? Esa pregunta exacta — cuándo devolverNoney cuándoraise— abre la próxima lección.
Conclusión
Ya sabes pedir perdón con estilo: try mínimo alrededor de la línea que puede fallar, except de tipos concretos (jamás pelado), varios except ordenados de lo específico a lo general, as e para aprovechar el mensaje del objeto excepción, else para el camino feliz que depende del éxito y finally para la limpieza incondicional. Papyrus ha pagado tres deudas: cargar_catalogo() distingue "no hay catálogo" (recuperable) de "catálogo corrupto" (mejor parar), cierre_de_caja() sobrevive a filas ilegibles contándolas en vez de morir o silenciarlas, y pedir_entero() jubila al impostor isdigit() dejando que int() sea su propio juez. Hasta ahora solo hemos reaccionado a excepciones que Python lanza por su cuenta. La próxima lección invierte los papeles: aprenderás a lanzarlas tú con raise — decidir cuándo una situación merece excepción y cuándo un simple None, relanzar, encadenar con from, y proteger tus funciones con guard clauses. El raise ValueError que el __post_init__ de Libro hace desde el módulo 5 dejará de ser un acto de fe para convertirse en una decisión de diseño.
Curso de Programación en Python
Módulo 1: Introducción a Python
- Introducción a Python
- Configuración del Entorno de Desarrollo
- Sintaxis de Python y Tipos de Datos Básicos
- Variables y Constantes
- Entrada y Salida Básica
- Entornos Virtuales y Gestión de Paquetes
Módulo 2: Estructuras de Control
- Sentencias Condicionales
- Bucles: for y while
- Herramientas de Control de Flujo
- Comprensiones de Listas
Módulo 3: Funciones y Módulos
- Definición de Funciones
- Argumentos de Función
- Funciones Lambda
- Módulos y Paquetes
- Visión General de la Biblioteca Estándar
Módulo 4: Estructuras de Datos
Módulo 5: Programación Orientada a Objetos
Módulo 6: Manejo de Archivos
- Lectura y Escritura de Archivos
- Trabajo con Archivos CSV
- Manejo de Datos JSON
- Operaciones con Archivos y Directorios
Módulo 7: Manejo de Errores y Excepciones
- Introducción a las Excepciones
- Manejo de Excepciones
- Lanzamiento de Excepciones
- Excepciones Personalizadas
- Buenas Prácticas y Registro de Errores con logging
Módulo 8: Temas Avanzados
- Anotaciones de Tipos (type hints)
- Decoradores
- Generadores
- Administradores de Contexto
- Concurrencia: Hilos y Procesos
- Asyncio para Programación Asíncrona
Módulo 9: Pruebas y Depuración
- Introducción a las Pruebas
- Pruebas Unitarias con unittest
- Pruebas con pytest
- Desarrollo Guiado por Pruebas
- Técnicas de Depuración
- Uso de pdb para Depuración
Módulo 10: Desarrollo Web con Python
- Introducción al Desarrollo Web
- Fundamentos del Framework Flask
- Construcción de APIs REST con Flask
- Introducción a Django
- Construcción de Aplicaciones Web con Django
Módulo 11: Ciencia de Datos con Python
- Introducción a la Ciencia de Datos
- NumPy para Computación Numérica
- Pandas para Manipulación de Datos
- Matplotlib para Visualización de Datos
- Introducción al Aprendizaje Automático con scikit-learn
