En Python las excepciones no son excepcionales: se usan para el flujo normal mucho más que en otros lenguajes. Esta lección va de usarlas bien, del with, y del except vacío, que es la peor costumbre que se puede coger.
Las que te vas a encontrar
| Código | Excepción y mensaje |
|---|---|
100 * (200/0) | ZeroDivisionError: division by zero |
5 + variable_sin_definir | NameError: name 'variable_sin_definir' is not defined |
'suma_total' + 10 | TypeError: can only concatenate str (not "int") to str |
int('hola') | ValueError: invalid literal for int() with base 10: 'hola' |
[1, 2][5] | IndexError: list index out of range |
{'a': 1}['b'] | KeyError: 'b' |
Son los mensajes que da Python 3.14 de verdad. Merece la pena leerlos enteros: casi siempre dicen qué pasó, y el nombre de la excepción dice de qué tipo es el problema. Además, cada excepción trae datos que se pueden usar:
try:
open("no_existe.txt")
except FileNotFoundError as e:
print("FileNotFoundError:", e.filename, "|", e.strerror)salidaFileNotFoundError: no_existe.txt | No such file or directory
try, except, else, finally
def convertir(s):
try:
n = int(s)
except ValueError:
return "no es un número"
except TypeError as e:
return f"ni siquiera es texto: {e}"
else:
return f"vale {n}" # solo si NO hubo excepción
finally:
... # siempre, incluso tras el returnsalida'7' -> vale 7 'x' -> no es un número None -> ni siquiera es texto: int() argument must be a string, a bytes-like object or a real number, not 'NoneType' ' 8 ' -> vale 8
Las cuatro partes, y para qué sirve cada una:
| Bloque | Cuándo se ejecuta |
|---|---|
try | siempre; es lo que puede fallar |
except | si saltó esa excepción |
else | si no saltó ninguna |
finally | siempre, haya error o no, incluso tras un return |
El else parece prescindible y no lo es: deja en el try solo la línea que puede fallar. Si metes ahí todo el trabajo, acabas capturando por accidente una excepción que venía de otro sitio.
Varios tipos de golpe
except (KeyError, IndexError) as e: # con "as", con paréntesis
...
except KeyError, IndexError: # sin "as": desde Python 3.14, sin paréntesis
...salidacon 'as', con paréntesis: KeyError 'x' sin 'as', ya no hacen falta los paréntesis
as. Lo comprobé: con as sigue dando «multiple exception types must be parenthesized when using ‘as’». Y en 3.13 la forma sin paréntesis no compila en ningún caso, así que no la uses si tu código tiene que correr en versiones anteriores.Excepciones propias, con datos dentro
class SaldoInsuficiente(Exception):
def __init__(self, faltan):
super().__init__(f"faltan {faltan} €")
self.faltan = faltansalidafaltan 30 € -> puedo decidir con e.faltan = 30
Eso es lo que distingue una excepción útil de un mensaje de texto: quien la recibe puede decidir con ella, no solo imprimirla. Y heredar de Exception, nunca de BaseException, que está reservada para cosas como el Ctrl+C.
Relanzar sin perder la causa
try:
int("x")
except ValueError as e:
raise RuntimeError("configuración inválida") from esalidaconfiguración inválida <- causada por ValueError: invalid literal for int() with base 10: 'x'
Ese from e es lo que hace que en la traza aparezca el «The above exception was the direct cause of…». Sin él, la pista original se pierde y quien depure el problema solo verá tu mensaje genérico.
with: cerrar sin acordarse
with p.open(encoding="utf-8") as f:
print(f.readline())
# aquí ya está cerrado, pase lo que pasesalidaprimera línea: 'una línea' ¿cerrado al salir del with? True
Todo lo que haya que cerrar o liberar —archivos, conexiones, cerrojos— se usa así. Es la versión corta y correcta de un try/finally, y además funciona si la excepción salta a mitad.
Uno propio, en cinco líneas
from contextlib import contextmanager
import time
@contextmanager
def cronometro(nombre):
t0 = time.perf_counter()
try:
yield
finally:
print(f"{nombre}: {(time.perf_counter()-t0)*1000:.1f} ms")
with cronometro("sumar un millón"):
sum(range(1_000_000))salidasumar un millón: 18.7 ms
Lo que va antes del yield se ejecuta al entrar, y lo de después al salir. El try/finally alrededor del yield no es opcional: sin él, una excepción dentro del with se saltaría la parte de limpieza.
Varias excepciones a la vez: except*
try:
raise ExceptionGroup("varios fallos", [ValueError("uno"), TypeError("dos")])
except* ValueError as g:
print("except* ValueError:", [str(x) for x in g.exceptions])
except* TypeError as g:
print("except* TypeError:", [str(x) for x in g.exceptions])salidaexcept* ValueError: ['uno'] except* TypeError: ['dos']
Desde Python 3.11. Aparece cuando lanzas varias tareas a la vez, por ejemplo con asyncio.TaskGroup, y fallan más de una: except*, con asterisco, recibe solo las de su tipo, y se ejecutan todos los bloques que coincidan, no solo el primero.
Lo que no hay que hacer
try:
algo_que_puede_fallar()
except Exception:
pass # el error desaparece y nadie se enteraexcept vacío no arregla el problema, lo esconde hasta que reaparece más lejos y más raro. Tres reglas que evitan casi todos los disgustos: captura el tipo concreto, no Exception a secas; si no vas a hacer nada con el error, déjalo subir; y no uses excepciones para el flujo normal cuando hay una comprobación directa —d.get(k) antes que un try alrededor de d[k].Dicho eso, en Python sí es idiomático «pedir perdón en vez de permiso»: intentar la operación y capturar si falla, en lugar de comprobar antes. Entre archivos y red, comprobar antes no sirve de mucho, porque las cosas cambian entre la comprobación y el uso.
| Situación | Qué se hace |
|---|---|
| Puede fallar y sabes arreglarlo | except TipoConcreto |
| Necesitas el objeto del error | except X as e |
| Algo tiene que cerrarse | with |
| Relanzar con otro tipo | raise Otro(...) from e |
| Un error propio del dominio | una clase que herede de Exception |
| No hay nada que hacer | dejarlo subir, nunca pass |
Todo el código de esta lección se ejecutó con Python 3.14 en un contenedor limpio antes de publicarla; las salidas y los errores están copiados de esa ejecución.
