Saltar al contenido
excepciones.py · devschool

Excepciones en Python

Lección 13 de 19 · 10 min de lectura · Actualizado el

En esta lección
  1. Leer un traceback
  2. Excepciones más comunes
  3. try y except
  4. else y finally
  5. Lanzar excepciones con raise
  6. Excepciones propias
  7. Validar lo que escribe el usuario
  8. EAFP frente a LBYL
  9. Errores frecuentes
  10. assert, en pocas palabras
  11. Resumen

Todo programa real se encuentra con situaciones imprevistas: el usuario escribe “veinte” donde esperabas un número, un fichero no existe, una lista llega vacía. En Python, cuando algo así ocurre se lanza una excepción, y si nadie la gestiona, el programa se detiene. En esta lección aprenderás a leer los mensajes de error, a capturar excepciones con try y except para que tu programa reaccione con cabeza, y a lanzar las tuyas propias.

Leer un traceback

Antes de capturar errores hay que saber leerlos. Este programa calcula el precio medio de una tienda que todavía no tiene productos:

def precio_medio(precios):
    return sum(precios) / len(precios)

def informe(tienda):
    media = precio_medio(tienda["precios"])
    print(f"Precio medio: {media}")

informe({"nombre": "Centro", "precios": []})

Al ejecutarlo aparece esto:

Traceback (most recent call last):
  File "tienda.py", line 8, in <module>
    informe({"nombre": "Centro", "precios": []})
  File "tienda.py", line 5, in informe
    media = precio_medio(tienda["precios"])
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "tienda.py", line 2, in precio_medio
    return sum(precios) / len(precios)
           ~~~~~~~~~~~~~^~~~~~~~~~~~~~
ZeroDivisionError: division by zero

Un traceback es el camino de llamadas que llevó al error. Se lee así:

  1. Empieza por la última línea: el tipo de excepción (ZeroDivisionError) y un mensaje (division by zero). Es lo más importante.
  2. Sube una línea: el archivo, la línea (line 2) y la función (precio_medio) donde saltó. Los símbolos ^ y ~ señalan la operación exacta.
  3. Sigue subiendo para ver quién llamó a quién: <module> (el programa principal) llamó a informe, que llamó a precio_medio.

“Most recent call last” significa que la llamada más reciente está abajo. Casi siempre el fallo está en la última línea de tu código que aparece en el traceback.

Excepciones más comunes

ExcepciónCuándo ocurreEjemplo
ValueErrorEl tipo es correcto pero el valor no sirveint("hola")
TypeErrorOperación con un tipo que no corresponde"Edad: " + 25
KeyErrorLa clave no existe en el diccionario{"a": 1}["b"]
IndexErrorLa posición no existe en la lista[1, 2][5]
ZeroDivisionErrorDivisión entre cero10 / 0
FileNotFoundErrorEl fichero no existeopen("no_existe.txt")
NameErrorUsas una variable que no está definidaprint(nombre)
AttributeErrorEl objeto no tiene ese método o atributo"hola".push("x")

Todas son clases que heredan de Exception. Aprenderte esta tabla te ahorrará mucho tiempo: el nombre del error ya te dice qué buscar.

try y except

El código que puede fallar se pone en un bloque try. Si se lanza una excepción, Python salta al bloque except correspondiente y el programa continúa:

def pedir(texto):
    try:
        n = int(texto)
        print(f"Tienes {n} años")
    except ValueError:
        print("Eso no es un número entero")

pedir("30")        # Tienes 30 años
pedir("treinta")   # Eso no es un número entero

En cuanto una línea del try falla, las siguientes no se ejecutan: por eso el segundo print del try no aparece con “treinta”.

Varios except

Puedes tratar cada tipo de error de forma distinta. Python prueba los except en orden y usa el primero que encaja:

def dividir(a, b):
    try:
        return a / b
    except ZeroDivisionError:
        print("No se puede dividir entre cero")
    except TypeError:
        print("Los dos valores deben ser números")
    return None

print(dividir(10, 2))     # 5.0
print(dividir(10, 0))     # No se puede dividir entre cero  /  None
print(dividir(10, "2"))   # Los dos valores deben ser números  /  None

Si varias excepciones se tratan igual, agrúpalas en una tupla:

def a_numero(valor):
    try:
        return float(valor)
    except (ValueError, TypeError):
        return 0.0

print(a_numero("3.5"), a_numero("abc"), a_numero(None))   # 3.5 0.0 0.0

Acceder al error con as

Con as guardas la excepción en una variable para mostrar su mensaje o registrarlo:

try:
    int("doce")
except ValueError as e:
    print("Error:", e)             # Error: invalid literal for int() with base 10: 'doce'
    print(type(e).__name__)        # ValueError

else y finally

El bloque try tiene dos partes opcionales más:

  • else: se ejecuta solo si no hubo excepción.
  • finally: se ejecuta siempre, haya error o no.
def leer_nota(texto):
    try:
        nota = float(texto)
    except ValueError:
        print(f"'{texto}' no es una nota válida")
    else:
        print(f"Nota guardada: {nota}")
    finally:
        print("Fin de la lectura")

leer_nota("7.5")
# Nota guardada: 7.5
# Fin de la lectura

leer_nota("siete")
# 'siete' no es una nota válida
# Fin de la lectura

¿Por qué no meter el print de else dentro del try? Porque conviene que el try contenga solo la línea que puede fallar. Si metes más código, podrías capturar por accidente un error de otra línea que no esperabas.

finally sirve para tareas de limpieza que deben hacerse sí o sí: cerrar una conexión, guardar un registro, liberar un recurso. Se ejecuta incluso si hay un return dentro del try:

def prueba():
    try:
        return "desde try"
    finally:
        print("finally se ejecuta igualmente")

print(prueba())
# finally se ejecuta igualmente
# desde try

Con ficheros normalmente no necesitarás finally, porque with cierra el fichero solo (lo verás en ficheros).

Lanzar excepciones con raise

Tus funciones también pueden avisar de que algo va mal. En lugar de devolver un valor raro como -1 o None, lanza una excepción con raise:

def retirar(saldo, cantidad):
    if cantidad <= 0:
        raise ValueError("La cantidad debe ser positiva")
    if cantidad > saldo:
        raise ValueError(f"Saldo insuficiente: tienes {saldo} €")
    return saldo - cantidad

print(retirar(100, 30))   # 70

try:
    retirar(100, 500)
except ValueError as e:
    print("No se pudo retirar:", e)
# No se pudo retirar: Saldo insuficiente: tienes 100 €

La ventaja es que el error no se puede ignorar sin querer: o alguien lo captura, o el programa se detiene con un mensaje claro.

Dentro de un except, un raise sin nada más vuelve a lanzar la misma excepción. Es útil si quieres registrar el error pero dejar que otro lo gestione.

Excepciones propias

Para errores de tu propio dominio (una tienda, un banco, un gimnasio) puedes crear tus excepciones heredando de Exception. Basta con una clase vacía con un docstring (las clases las verás más adelante):

class SaldoInsuficienteError(Exception):
    """Se lanza cuando no hay dinero suficiente en la cuenta."""

class CuentaBloqueadaError(Exception):
    """Se lanza al operar con una cuenta bloqueada."""

def retirar(saldo, cantidad, bloqueada=False):
    if bloqueada:
        raise CuentaBloqueadaError("La cuenta está bloqueada")
    if cantidad > saldo:
        raise SaldoInsuficienteError(f"Faltan {cantidad - saldo} €")
    return saldo - cantidad

try:
    retirar(50, 80)
except SaldoInsuficienteError as e:
    print("Saldo:", e)          # Saldo: Faltan 30 €
except CuentaBloqueadaError as e:
    print("Bloqueo:", e)

Así quien use tu código puede distinguir cada situación y actuar en consecuencia. Por convención, sus nombres terminan en Error.

Si creas una excepción base para tu aplicación y haces que las demás hereden de ella, podrás capturarlas todas de golpe:

class ErrorTienda(Exception): pass
class SinStockError(ErrorTienda): pass
class CuponInvalidoError(ErrorTienda): pass

try:
    raise SinStockError("No quedan camisetas")
except ErrorTienda as e:
    print(type(e).__name__, e)   # SinStockError No quedan camisetas

Validar lo que escribe el usuario

Uno de los usos más habituales: pedir un dato con input hasta que sea válido. Se combina un bucle while True con try:

def pedir_edad():
    while True:
        texto = input("¿Cuántos años tienes? ")
        try:
            edad = int(texto)
        except ValueError:
            print("Escribe un número entero, por ejemplo 25.")
            continue
        if edad < 0 or edad > 120:
            print("La edad debe estar entre 0 y 120.")
            continue
        return edad

edad = pedir_edad()
print(f"Edad registrada: {edad}")
¿Cuántos años tienes? veinte
Escribe un número entero, por ejemplo 25.
¿Cuántos años tienes? -5
La edad debe estar entre 0 y 120.
¿Cuántos años tienes? 20
Edad registrada: 20

continue vuelve al principio del bucle y return sale de la función con el valor bueno.

EAFP frente a LBYL

Hay dos estilos para tratar situaciones que pueden fallar:

  • LBYL (Look Before You Leap, “mira antes de saltar”): compruebas antes con un if.
  • EAFP (Easier to Ask Forgiveness than Permission, “es más fácil pedir perdón que permiso”): lo intentas y capturas el error si falla.
precios = {"pan": 1.2}

# LBYL
if "leche" in precios:
    precio = precios["leche"]
else:
    precio = 0

# EAFP
try:
    precio = precios["leche"]
except KeyError:
    precio = 0

En Python se considera más natural el estilo EAFP, sobre todo cuando comprobar antes es difícil o poco fiable (por ejemplo, si un fichero existe: podría borrarse entre la comprobación y la apertura). Aun así, usa el sentido común: si hay una forma directa, como precios.get("leche", 0), es mejor que ambas.

Errores frecuentes

No uses except desnudo

try:
    total = calcular()
except:              # mal
    print("Algo ha fallado")

Un except: sin tipo captura absolutamente todo, incluidos los errores de escritura de tu propio código (un NameError por una errata) y hasta Ctrl+C. El programa sigue como si nada y nunca sabrás qué ha pasado. Captura siempre el tipo concreto que esperas. Si de verdad necesitas algo general, usa except Exception as e: y muestra o registra e.

Otros descuidos

  • Silenciar errores con pass: except ValueError: pass esconde el problema. Al menos informa de él.
  • Meter demasiado código en el try: captura solo lo que puede fallar.
  • Orden incorrecto de los except: si pones except Exception primero, los más concretos de debajo nunca se ejecutan.
  • Usar excepciones como if: si una simple comparación resuelve el caso, no hace falta try.

assert, en pocas palabras

assert comprueba algo que debería ser siempre cierto y lanza AssertionError si no lo es:

def media(notas):
    assert len(notas) > 0, "La lista de notas está vacía"
    return sum(notas) / len(notas)

media([])   # AssertionError: La lista de notas está vacía

Úsalo para detectar errores de programación mientras desarrollas y en los tests. No lo uses para validar datos del usuario: Python puede ejecutarse con la opción -O, que elimina todos los assert. Para validar, usa if y raise.

Resumen

ElementoPara qué sirve
TracebackMuestra el tipo de error, el mensaje y el camino de llamadas; se lee de abajo arriba
tryContiene el código que puede fallar
except TipoGestiona un tipo concreto de error
except (A, B)Gestiona varios tipos igual
as eGuarda la excepción para ver su mensaje
elseSe ejecuta si no hubo error
finallySe ejecuta siempre
raiseLanza una excepción
class MiError(Exception)Crea una excepción propia
assertComprueba condiciones internas mientras desarrollas

Regla de oro: captura solo lo que sabes tratar y deja que lo demás salte con un mensaje claro. En la siguiente lección aprenderás a organizar tu código en módulos.

Pon a prueba lo que has aprendido

[Python] ¿Qué imprime f(0)?
def f(x):
    try:
        r = 10 / x
    except ZeroDivisionError:
        print("A")
    else:
        print("B")
    finally:
        print("C")

f(0)

[Python] ¿Qué imprime este código?
try:
    n = int("5") + "1"
except ValueError:
    print("valor")
except TypeError:
    print("tipo")

[Python] En un traceback de Python, ¿dónde está normalmente la información más importante?

[Python] ¿Por qué se desaconseja escribir except: sin indicar ningún tipo?

¿Te ha quedado claro? Márcala y verás tu progreso en el explorador.