Excepciones — manejar lo que falla
Capturar un error concreto con try/except, por qué `except Exception: pass` casi siempre está mal, `else` y `finally`, lanzar tus propios errores y elegir un error del tamaño adecuado entre la familia.
¿Cómo quieres aprender?
El problema que vamos a resolver
La última pregunta del capítulo anterior hacía que un programa procesara tres líneas correctamente y se detuviera en la cuarta. En versión pequeña, se ve así:
numbers = ["10", "twenty", "30"]
total = 0
for text in numbers:
total += int(text)
print(total)ValueError: invalid literal for int() with base 10: 'twenty'(Antes de la línea del error Python también imprime unas líneas de rastreo; en este capítulo casi siempre solo se muestra la última.)
10 se sumó correctamente. 30 nunca se sumó. Y print(total) nunca se ejecutó.
Una línea mala detuvo todo el trabajo. Hasta ahora solo hemos tenido dos opciones — o cada dato es perfecto, o el programa se detiene.
numbers = ["10", "twenty", "30"]
total = 0
for text in numbers:
try:
total += int(text)
except ValueError:
print("skipping", text)
print(total)skipping twenty
40Hay una tercera opción: deja que el problema ocurra, y luego respóndelo.
Al terminar este capítulo podrás
- Capturar un error concreto con
try/except - Explicar por qué un
except:vacío o unexcept Exception: passcasi siempre está mal - Tomar el error con
as erry leer su mensaje - Explicar qué hacen
elseyfinally raisetus propios errores, y decidir cuándo hacerlo- Reconocer la familia de los errores, y capturar uno del tamaño adecuado
Requisitos previos: Lectura y escritura de archivos.
Un error también es un objeto
Escribir as pone el error en tus manos:
try:
int("twenty")
except ValueError as err:
print(type(err))
print(err)<class 'ValueError'>
invalid literal for int() with base 10: 'twenty'El texto rojo que has estado viendo en la terminal todo este tiempo son estos objetos, e imprimir uno da su mensaje.
Hay una consecuencia práctica: el error trae la mejor explicación que existe. Imprimir err casi siempre es más útil que escribir "algo salió mal" con tus propias palabras.
Captura el error correcto
try:
int("twenty")
except TypeError:
print("caught")ValueError: invalid literal for int() with base 10: 'twenty'Un except solo captura el tipo que nombra. Este nombra TypeError, y lo que pasó fue un ValueError — así que el bloque except podría no estar ahí.
Puede parecer que estorba, y esa es la parte útil. La línea except dice qué error tenías previsto — y lo que no tenías previsto sigue deteniendo el programa, que es lo correcto.
Por eso no deberías escribir esto:
numbers = ["10", "twenty", "30"]
total = 0
for text in numbers:
try:
total += int(text)
except Exception:
pass
print(total)40La respuesta es correcta y el código sigue siendo malo. except Exception: pass se traga todo — los fallos que previste y los que no. Cuando mañana numbers se convierta accidentalmente en un diccionario, o un nombre dentro esté mal escrito, este programa dará una respuesta incorrecta en silencio.
La regla: captura exactamente el error para el que tienes un plan. Y una vez capturado, haz algo — como mínimo, di qué pasó.
Más de un tipo
def get(data, key):
try:
return data[key]
except KeyError:
return "no such key"
except IndexError:
return "no such position"
print(get({"pen": 15}, "pen"))
print(get({"pen": 15}, "bag"))
print(get([1, 2, 3], 9))15
no such key
no such positionVarias cláusulas except pueden seguirse unas a otras, y se ejecuta la primera que coincide — las demás ni se miran.
Cuando la respuesta es la misma, se pueden escribir juntas:
for value in ["10", None, "x"]:
try:
print(int(value))
except (ValueError, TypeError) as err:
print(type(err).__name__, "-", err)10
TypeError - int() argument must be a string, a bytes-like object or a real number, not 'NoneType'
ValueError - invalid literal for int() with base 10: 'x'type(err).__name__ da el nombre del tipo de error — útil al escribir registros.
Los errores vienen en familias
print(issubclass(FileNotFoundError, OSError))
print(issubclass(ValueError, Exception))
print(issubclass(KeyError, LookupError))
print(issubclass(IndexError, LookupError))True
True
True
TrueLos errores están organizados como un árbol, y capturar un padre captura a sus hijos:
try:
raise FileNotFoundError("gone")
except OSError as err:
print("caught as OSError:", err)caught as OSError: goneAlgunas relaciones que conviene conocer: FileNotFoundError y PermissionError son ambos OSError; KeyError y IndexError son ambos LookupError; y casi todo es un Exception.
De ahí se desprende la regla: captura el error más pequeño que puedas. except Exception es lo más fácil de escribir y lo menos útil.
else y finally
def read(text):
try:
value = int(text)
except ValueError:
print("bad:", text)
return None
else:
print("good:", value)
return value
finally:
print("done with", text)
print(read("10"))
print(read("ten"))good: 10
done with 10
10
bad: ten
done with ten
Noneelse se ejecuta cuando nada dentro del bloque try falló. Permite que lo de dentro de try se quede pequeño — solo la línea que puede fallar —, con el trabajo que sigue al éxito en else. Mete demasiado dentro de try y capturarás cosas que nunca quisiste.
finally siempre se ejecuta, pase lo que pase. Incluso después de un return:
def risky():
try:
return "from try"
finally:
print("finally still runs")
print(risky())finally still runs
from tryEl return ya ha fijado el valor, pero finally se ejecuta antes de salir de la función. Es el lugar para limpiar — aunque para los archivos, with ya hace ese trabajo.
Lanzar tus propios errores
Lanzar importa tanto como capturar.
def line_total(price, quantity):
if quantity < 0:
raise ValueError(f"quantity cannot be negative: {quantity}")
return price * quantity
print(line_total(15.0, 3))
print(line_total(15.0, -1))45.0
ValueError: quantity cannot be negative: -1¿Cuál era la alternativa a raise? Devolver None, o 0 — y entonces el fallo viajaría. Se colaría en algún total y saldría a la superficie mucho después como un número extraño sin origen rastreable.
Un error detiene el fallo donde nació, y dice por qué en su mensaje.
Al escribir ese mensaje, incluye dentro el valor problemático. "invalid quantity" y "quantity cannot be negative: -1" — solo la segunda te deja empezar a trabajar.
Un error también se puede capturar y volver a lanzar con un mejor mensaje. parse.py:
def parse(text):
try:
return int(text)
except ValueError:
raise ValueError(f"not a number: {text!r}")
print(parse("10"))
print(parse("ten"))10
Traceback (most recent call last):
File ".../parse.py", line 3, in parse
return int(text)
^^^^^^^^^
ValueError: invalid literal for int() with base 10: 'ten'
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File ".../parse.py", line 9, in <module>
print(parse("ten"))
^^^^^^^^^^^^
File ".../parse.py", line 5, in parse
raise ValueError(f"not a number: {text!r}")
ValueError: not a number: 'ten'Esta vez se muestra la salida completa, porque aquí la última línea no cuenta toda la historia. (En tu máquina las líneas File contendrán la ruta completa del archivo; aquí se han acortado a ....) Python imprimió dos rastreos: el primero es el error original de int(), el segundo es nuestro nuevo ValueError. La línea intermedia, During handling of the above exception, another exception occurred:, dice que el segundo ocurrió mientras se manejaba el primero. No se pierde información — pero se lee como si hubiera un fallo dentro del bloque except.
Cuando lanzas un nuevo error a propósito, dilo: escribe except ValueError as err: y luego raise ValueError(f"not a number: {text!r}") from err. La línea intermedia pasa a ser The above exception was the direct cause of the following exception: — es decir, "el primero causó el segundo", que es exactamente lo que queremos decir.
{text!r} inserta el valor a través de repr, así que las comillas se ven — y la diferencia entre not a number: 'ten' y not a number: ten importa cuando el valor es de verdad una cadena vacía o un espacio sobrante.
Preguntar primero o pedir perdón después
prices = {"pen": 15}
if "bag" in prices:
print(prices["bag"])
else:
print("not stocked")
try:
print(prices["bag"])
except KeyError:
print("not stocked")not stocked
not stockedLas dos están bien. La segunda es más común en Python, y por una razón: la primera mira dos veces — una en in, otra en [...] — y aun así puede fallar si algo cambia en medio.
Para lo que pasa normalmente, sin embargo, un if se lee mejor. Las excepciones son para lo excepcional; el nombre lo dice.
Un ejemplo completo
orders.txt, con dos líneas malas a propósito:
pen,15.0,3
bag,eight,1
ink,120.0,2
clip,5.0main.py:
"""Read an order file, and keep going when a line is wrong."""
from pathlib import Path
TAX_RATE = 0.15
def parse_line(raw, number):
"""Returns one order line, or raises ValueError saying which line was wrong."""
parts = raw.split(",")
if len(parts) != 3:
raise ValueError(f"line {number}: expected 3 fields, got {len(parts)}")
name, price, quantity = parts
try:
return name, float(price), int(quantity)
except ValueError as err:
raise ValueError(f"line {number}: {err}")
def read_order(path):
"""Returns the good lines and the complaints, never raising for bad data."""
try:
text = path.read_text(encoding="utf-8")
except FileNotFoundError:
return [], [f"no such file: {path}"]
lines = []
problems = []
for number, raw in enumerate(text.splitlines(), start=1):
if not raw.strip():
continue
try:
lines.append(parse_line(raw, number))
except ValueError as err:
problems.append(str(err))
return lines, problems
def main():
lines, problems = read_order(Path("orders.txt"))
total = 0.0
for name, price, quantity in lines:
amount = round(price * quantity * (1 + TAX_RATE), 2)
total += amount
print(f"{name:<6} {amount:>9.2f}")
print(f"{'total':<6} {total:>9.2f}")
print()
print(f"{len(lines)} lines used, {len(problems)} skipped")
for problem in problems:
print(" -", problem)
if __name__ == "__main__":
main()pen 51.75
ink 276.00
total 327.75
2 lines used, 2 skipped
- line 2: could not convert string to float: 'eight'
- line 5: expected 3 fields, got 2Cuatro cosas que merece la pena ver.
parse_line lanza y read_order captura. La función de abajo no tiene idea de qué debe hacer el programa en su conjunto — solo sabe que esta línea es inutilizable. Decidir qué hacer con eso le corresponde a la función de arriba. Los errores se lanzan abajo y se capturan arriba, y esa división es todo el trabajo de un sistema de excepciones.
El número de línea está dentro del mensaje. could not convert string to float: 'eight' no dice dónde mirar; line 2: ... sí. Todo lo que sabes en el momento de capturar y no sabrás después va en el mensaje.
read_order nunca se rompe. Devuelve dos cosas — lo que se pudo usar y lo que no. Si falta el archivo, se produce una respuesta de la misma forma, así que main no necesita dos caminos distintos.
El bloque try interior mide exactamente una línea. float(price) e int(quantity) — solo la parte que puede fallar está dentro. parts = raw.split(",") está fuera, porque no debería fallar, y si lo hace quiero saberlo.
Cuando falla
El error no se captura, aunque haya un except El tipo no coincide. La primera palabra de la última línea del mensaje es el nombre del error — nombra ese, o captura su padre.
Hay un except y el programa se detiene igual La línea que falla no está dentro del bloque try. Revisa la indentación.
SyntaxError: expected 'except' or 'finally' block Se escribió un try sin except. try no puede ir solo.
Todo "funciona" pero las respuestas son incorrectas Hay un except Exception: pass en algún lado. Encuéntralo — ahí está el problema.
Lancé algo pero no aparece ningún mensaje Se escribió raise ValueError en lugar de raise ValueError("..."). Añade los paréntesis y el mensaje.
Un return dentro de finally pierde el error Sí, y por eso no deberías escribir uno ahí. finally es solo para limpiar.
Paso 4 de 6 — Predice
Comprueba lo que has entendido
Hay un except. ¿Qué le pasa al programa?
try:
int("twenty")
except TypeError:
print("caught")- ASe imprime `caught`
- BNo se imprime nada y el programa termina en silencio
- CSe detiene con un `TypeError`
- DEl programa se detiene con un `ValueError` — el `except` podría no estar ahí
Todo el bucle está dentro del try. ¿Qué se imprime?
values = ["10", "x", "30"]
total = 0
try:
for text in values:
total += int(text)
except ValueError:
print("gave up")
print(total)- Agave up 40
- Bgave up 10
- C40
- Dgave up 0
Tanto el try como el except tienen un return. ¿Qué se imprime?
def read(text):
try:
return int(text)
except ValueError:
return 0
finally:
print("checked", text)
print(read("7"))
print(read("seven"))- Achecked 7 7 checked seven 0
- B7 0
- Cchecked 7 7 0
- D7 checked 7 0 checked seven
Para responder necesitas una cuenta
Inicia sesión para comprobar tus respuestas
Las preguntas están arriba, y resolverlas mentalmente es lo que de verdad importa. Inicia sesión para ver las respuestas, las explicaciones y las pistas de tres niveles.
Tu turno
Vuelve al summary.py del capítulo anterior, el que leía people.txt.
Ahora haz que nunca se caiga, y que termine diciendo qué líneas se omitieron y por qué. Escribe un parse_line(raw, number) que lance un ValueError con el número de línea dentro cuando la línea sea mala.
Luego daña el archivo a propósito — quita una coma de una línea, pon letras donde va un número en otra, y deja una línea completamente vacía. Por ejemplo:
rafi,12
nadia 30
sami,ten
lina,25La línea vacía no es una queja — como en el capítulo anterior, se omite en silencio. Así que el programa debe llegar hasta el final e informar de dos quejas. Si escribes los mensajes como lo hace el ejemplo completo (expected 2 fields, got ... y line {number}: {err}), el final de la salida será exactamente este:
2 lines used, 2 skipped
- line 2: expected 2 fields, got 1
- line 3: invalid literal for int() with base 10: 'ten'Luego, cuatro experimentos:
- Cambia
except ValueErrorporexcept Exception: pass. ¿Qué salida da, y por qué es más peligroso? - Quita el número de línea del mensaje de
parse_line. ¿Cuánto cuesta ahora encontrar una línea mala? - Haz el bloque
trymás grande — mete dentro todo el cuerpo del bucle. ¿Qué pasa ahora cuando una línea es mala? - Añade un
finallyque imprima algo después de cada línea, y comprueba que se ejecuta tanto en las buenas como en las malas.
El tercer experimento enseña el hábito más útil de este capítulo: cuanto más pequeño es el bloque try, más honesta es la captura.
Paso 6 de 6
Reto — el quiz del capítulo
Diez preguntas de fácil a difícil. Las últimas son difíciles a propósito.
Inicia sesión para hacer el quiz