Saltar al contenido principal

Excepciones

Cuando un programa se topa con algo que no sabe resolver por sí solo (abrir un archivo que no existe, dividir entre cero, acceder a una posición de una lista que no existe), Python interrumpe lo que estaba haciendo y lanza una excepción: un aviso de que ha surgido un problema durante la ejecución (runtime). Si nadie la atiende, el programa se detiene ahí mismo con un error.

La ventaja de que estos problemas se expresen como excepciones es que se pueden capturar: en lugar de dejar que el programa se rompa sin más, el propio código puede reaccionar ante ellas (mostrar un mensaje claro, pedir el dato de nuevo, usar un valor por defecto...) y decidir cómo seguir adelante. El resto de este apartado explica cómo capturar excepciones, cómo distinguir unas de otras y cómo lanzarlas tú mismo desde tus propias funciones.

Captura de excepciones

Anteriormente vimos fragmentos de código donde usábamos las funciones input e int para leer y analizar un número entero introducido por el usuario. Aquí un ejemplo para convertir una temperatura de grados Fahrenheit a grados Celsius:

ent = input('Introduzca la Temperatura Fahrenheit:')
fahr = float(ent)
cel = (fahr - 32.0) * 5.0 / 9.0
print(cel)

Si ejecutamos este código y le damos una entrada no válida, simplemente fallará con un mensaje de error:

Introduzca la Temperatura Fahrenheit:72
22.2222222222
Introduzca la Temperatura Fahrenheit:fred
Traceback (most recent call last):
File "fahren.py", line 2, in <module>
fahr = float(ent)
ValueError: invalid literal for float(): fred

Existen estructuras de ejecución condicional dentro de Python para manejar este tipo de errores esperados e inesperados, llamadas try/except. La idea de try y except es que si se sabe que cierta secuencia de instrucciones puede generar un problema, es posible añadir ciertas sentencias para que sean ejecutadas en caso de error. Estas sentencias extra (el bloque except) serán ignoradas si no se produce ningún error.

Podemos reescribir nuestro conversor de temperatura de esta forma:

ent = input('Introduzca la Temperatura Fahrenheit:')
try:
fahr = float(ent)
cel = (fahr - 32.0) * 5.0 / 9.0
print(cel)
except:
print('Por favor, introduzca un número')

Python comienza ejecutando la secuencia de sentencias del bloque try. Si todo va bien, se saltará todo el bloque except y terminará. Si ocurre una excepción dentro del bloque try, Python saltará fuera de ese bloque y se ejecutará la secuencia de sentencias del bloque except.

Introduzca la Temperatura Fahrenheit:72
22.2222222222
Introduzca la Temperatura Fahrenheit:a
Por favor, introduzca un número

Gestionar una excepción con una sentencia try recibe el nombre de capturar una excepción. En este ejemplo, la cláusula except muestra un mensaje de error. En general, capturar una excepción te da la oportunidad de corregir el problema, volver a intentarlo, o al menos, terminar el programa con elegancia.

Dentro de la cláusula except podemos tener más de una sentencia, y esta puede realizar cualquier acción:

ent = input('Introduzca la Temperatura Fahrenheit:')

try:
fahr = float(ent)
cel = (fahr - 32.0) * 5.0 / 9.0
print(cel)
except:
print("Ocurrió un error")
print('Por favor, introduzca un número')
Reversión de sentencias

En Python, cuando se produce una excepción en un bloque try/except, las sentencias anteriores dentro del bloque hasta el punto donde se generó la excepción no son revertidas.

Por ejemplo:

sin_reversion = 1

try:
sin_reversion = 2
error = 5/0
sin_reversion = 3
except:
print("Se produjo un error")

print(sin_reversion) # Imprime: 2

Propagación

La propagación de excepciones en Python se refiere al comportamiento de las excepciones cuando estas no son capturadas en un bloque de código y "se propagan" hacia otros bloques, hasta que encuentran un try-except que las capture, o hasta que llegan al nivel más alto del programa, lo que genera un mensaje de error y finaliza la ejecución del programa.

  1. Generación de una excepción:

    Una excepción ocurre cuando se detecta un error durante la ejecución del código. Por ejemplo, dividir por cero o intentar acceder a un índice fuera de rango genera una excepción.

    x = 1 / 0 # Esto genera una excepción
  2. Búsqueda de un bloque try-except:

    Cuando ocurre una excepción, Python busca un bloque try-except en el código actual que pueda manejar ese tipo de excepción. Si encuentra uno, la ejecución salta al bloque except correspondiente y el resto del bloque try se omite.

    try:
    x = 1 / 0
    except:
    print("¡No se puede dividir por cero!")

    En este caso, la excepción es capturada y manejada correctamente, imprimiendo el mensaje "¡No se puede dividir por cero!".

  3. Propagación de la excepción:

    Si la excepción no se captura en el bloque try-except actual, la excepción se propaga hacia afuera, buscando en el siguiente nivel de la pila de llamadas (otro bloque try-except en funciones que llamaron a la función actual) hasta que encuentra un manejador que pueda capturarla.

    def funcion_b():
    x = 1 / 0 # Excepción aquí

    def funcion_a():
    funcion_b() # Llama a funcion_b

    try:
    funcion_a() # Llama a funcion_a
    except:
    print("¡Capturada en la función principal!")

    Aquí, la excepción se produce en funcion_b y, como no hay un manejador para capturarla allí, la excepción se propaga hacia funcion_a y, finalmente, hasta el bloque try-except en la función principal, donde es capturada y manejada.

  4. Excepción no capturada:

    Si la excepción no se captura en ningún nivel de la pila de llamadas, llega al nivel más alto del programa. En este caso, Python mostrará un mensaje de error (el traceback) y finalizará la ejecución del programa.

    def funcion_b():
    x = 1 / 0

    def funcion_a():
    funcion_b()

    funcion_a() # Sin bloque try-except

    # Resultado:
    # ZeroDivisionError: division by zero

    En este caso, como no hay ningún bloque try-except, la excepción no se captura y el programa termina con un error.

Ejemplos

Tenemos el siguiente programa:

def funcion_b():
x = 1 / 0
return x

def funcion_a():
a = funcion_b()
return a

var = funcion_a()
print(var)

Como vimos, al no capturar la excepción, se provoca un error en la ejecución del programa. Por lo tanto debemos capturar la excepción. Tenemos varios lugares donde capturarla. Por ejemplo, capturémosla en el programa principal:

def funcion_b():
x = 1 / 0
return x

def funcion_a():
a = funcion_b()
return a

try:
var = funcion_a()
except:
var = 1
print(var)

En este caso, la instrucción x = 1 / 0 provoca una excepción. Al no capturarse, esta se propaga a la función que llamó a esta función, en este caso funcion_a() en la instrucción a = funcion_b(). Esta línea tampoco es capturada por una excepción, entonces se propaga a la función que llamó a esta, que en este caso es el programa principal. Aquí sí que se captura, y por lo tanto se ejecuta el bloque de código del except y el programa imprimirá el valor 1.

Pero podemos capturar la excepción en otro lugar, como puede ser en la funcion_a():

def funcion_b():
x = 1 / 0
return x

def funcion_a():
try:
a = funcion_b()
except:
a = 2
return a

var = funcion_a()
print(var)

En este caso, la instrucción x = 1 / 0 provoca una excepción. Al no capturarse, esta se propaga a la función que llamó a esta función, en este caso funcion_a() en la instrucción a = funcion_b(). En esta línea se captura la excepción, por lo que a la variable a se le asigna el valor 2. El programa continúa su ejecución normal y por lo tanto imprimirá el valor 2.

También es posible capturar la excepción en la funcion_b() donde se provoca:

def funcion_b():
try:
x = 1 / 0
except:
x = 3
return x

def funcion_a():
a = funcion_b()
return a

var = funcion_a()
print(var)

Del mismo modo que en los casos anteriores x = 1 / 0 provoca una excepción, pero en este caso ya se captura en la propia funcion_b(), por lo que esta función devuelve el valor 3. El programa continúa su funcionamiento normal, por lo que se imprimirá el valor 3.

Pero, ¿qué pasa si intentamos capturar una excepción varias veces? Pues no es posible, porque una excepción sólo se puede capturar una única vez.

Supongamos este ejemplo que es una mezcla de los anteriores:

def funcion_b():
try:
x = 1 / 0
except:
x = 3
return x

def funcion_a():
try:
a = funcion_b()
except:
a = 2
return a

try:
var = funcion_a()
except:
var = 1
print(var)

La excepción provocada por la instrucción x = 1 / 0 ya es capturada en la propia funcion_b, por lo que los bloques try de funcion_a y del programa principal no capturarán esta excepción. Por lo tanto, el programa imprimirá el valor 3.

Tipos de excepciones

En Python, y en la mayoría de lenguajes que utilizan excepciones, hay diferentes tipos de excepciones. El tipo se imprime como parte del mensaje de error. Algunos de los más comunes son:

  • ZeroDivisionError: se produce cuando se intenta realizar una división entre 0.

    10 / 0 # ZeroDivisionError: division by zero
  • TypeError: ocurre cuando una operación o función se aplica a un valor de tipo no válido. Por ejemplo, cuando queremos concatenar cadenas de texto con números.

    "Edad: " + 25 # TypeError: can only concatenate str (not "int") to str
  • ValueError: se genera cuando una operación o función recibe un argumento que tiene el tipo correcto pero un valor no apropiado.

    int("hola") # ValueError: invalid literal for int() with base 10: 'hola'

El tipo es el nombre de la excepción que ocurrió. Esto es válido para todas las excepciones predefinidas por el intérprete así como para aquellas que son definidas por los usuarios. Los nombres de las excepciones también actúan como palabras reservadas del lenguaje.

Junto con el nombre de la excepción, generalmente se aporta más información sobre la causa de esta excepción.

En la documentación oficial podemos encontrar las excepciones predefinidas y por qué se producen:

BaseException
├── BaseExceptionGroup
├── GeneratorExit
├── KeyboardInterrupt
├── SystemExit
└── Exception
├── ArithmeticError
│ ├── FloatingPointError
│ ├── OverflowError
│ └── ZeroDivisionError
├── AssertionError
├── AttributeError
├── BufferError
├── EOFError
├── ExceptionGroup [BaseExceptionGroup]
├── ImportError
│ └── ModuleNotFoundError
├── LookupError
│ ├── IndexError
│ └── KeyError
├── MemoryError
├── NameError
│ └── UnboundLocalError
├── OSError
│ ├── BlockingIOError
│ ├── ChildProcessError
│ ├── ConnectionError
│ │ ├── BrokenPipeError
│ │ ├── ConnectionAbortedError
│ │ ├── ConnectionRefusedError
│ │ └── ConnectionResetError
│ ├── FileExistsError
│ ├── FileNotFoundError
│ ├── InterruptedError
│ ├── IsADirectoryError
│ ├── NotADirectoryError
│ ├── PermissionError
│ ├── ProcessLookupError
│ └── TimeoutError
├── ReferenceError
├── RuntimeError
│ ├── NotImplementedError
│ ├── PythonFinalizationError
│ └── RecursionError
├── StopAsyncIteration
├── StopIteration
├── SyntaxError
│ └── IndentationError
│ └── TabError
├── SystemError
├── TypeError
├── ValueError
│ └── UnicodeError
│ ├── UnicodeDecodeError
│ ├── UnicodeEncodeError
│ └── UnicodeTranslateError
└── Warning
├── BytesWarning
├── DeprecationWarning
├── EncodingWarning
├── FutureWarning
├── ImportWarning
├── PendingDeprecationWarning
├── ResourceWarning
├── RuntimeWarning
├── SyntaxWarning
├── UnicodeWarning
└── UserWarning

Captura de una excepción concreta

En el punto anterior vimos cómo capturar cualquier excepción. En este caso, procederemos a capturar un tipo de excepción concreto.

Para ello, tras la cláusula except añadimos el tipo de excepción que queremos capturar:

try:
x = int(input("Introduce un número: "))
except ValueError:
print("El valor introducido no se corresponde con un valor numérico")

En este caso, solo se capturarán errores dentro del bloque try de tipo ValueError.

Una declaración try puede tener más de una cláusula except, para especificar diferentes gestiones para diferentes excepciones:

try:
x = int(input("Introduce un número: "))
z = 10 / x
except ValueError:
print("El valor introducido no se corresponde con un valor numérico")
except ZeroDivisionError:
print("Se produjo una división entre 0")

Como máximo, solo se ejecutará un bloque except. Cuando uno de estos se ejecuta, directamente se salta todo el bloque try con sus correspondientes except.

Además, una cláusula except también puede gestionar varias excepciones añadiéndolas entre paréntesis y separadas por comas tras la directiva except:

try:
x = int(input("Introduce un número: "))
z = 10 / x
except (ValueError, ZeroDivisionError):
print("El valor introducido no se corresponde con un valor numérico o se produjo una división entre 0")

Cláusulas else y finally

else

En una captura de excepciones en Python, se puede utilizar la cláusula else para definir un bloque de código que se ejecutará si no se produce ninguna excepción dentro del bloque try.

Aquí hay un ejemplo para ilustrar el uso de else en un bloque try/except:

def dividir(a, b):
try:
resultado = a / b
except ZeroDivisionError:
print("Error: No se puede dividir por cero.")
else:
print(f"El resultado de {a} / {b} es: {resultado}")

dividir(10, 2) # Imprime: El resultado de 10 / 2 es: 5.0

dividir(10, 0) # Error: No se puede dividir por cero.

finally

La cláusula finally en Python se utiliza para definir un bloque de código que se ejecutará siempre tras el bloque try, se produzca una excepción o no.

Ejemplo:

def dividir(a, b):
try:
resultado = a / b
except ZeroDivisionError:
print("Error: No se puede dividir por cero.")
else:
print(f"El resultado de {a} / {b} es: {resultado}")
finally:
print("Este bloque se ejecuta siempre.")

dividir(10, 2)
# Salida:
# El resultado de 10 / 2 es: 5.0
# Este bloque se ejecuta siempre.

dividir(10, 0)
# Salida:
# Error: No se puede dividir por cero.
# Este bloque se ejecuta siempre.

Lanzar excepciones con raise

En apartados anteriores vimos que dentro de las funciones podían producirse errores y que estos se podían controlar mediante captura de excepciones. En este capítulo veremos cómo se indica dentro de una función que se produjo un error. Esto se conoce como lanzar una excepción.

La declaración raise permite al programador forzar que ocurra una excepción específica.

Imaginemos que creamos una función que calcula la raíz cuadrada de un número que se pasa como argumento. Si el número proporcionado es un número negativo, podemos lanzar una excepción de tipo ValueError. Ejemplo:

def raiz_cuadrada(numero):
"""
Calcula la raíz cuadrada de un número

Args:
numero (int): el número del que se realiza la raíz cuadrada

Returns:
float: el valor de la raíz cuadrada

Raises:
ValueError: Si el número es negativo
"""
if numero < 0:
raise ValueError
else:
return numero ** (1/2)
DocString

Las excepciones que una función o método puede lanzar se pueden documentar dentro del docstring utilizando la etiqueta Raises.

En Python, siempre que implementemos una función, deberemos realizar siempre las siguientes comprobaciones en las primeras líneas de código y en el siguiente orden:

  1. Comprobar tipo de datos de los parámetros. Debemos comprobar que los parámetros que recibimos son válidos. En caso contrario, lanzaremos una excepción de tipo TypeError. Por ejemplo, tenemos una función que calcula el precio con IVA de un producto a partir del precio sin él que se nos pasa por parámetro. Si este precio no es un número (es decir, un int o un float) lanzaremos la excepción TypeError.
  2. Comprobar que los valores de los parámetros son válidos. También tendremos que comprobar que los valores que se pasan por parámetros sean adecuados para poder realizar las operaciones necesarias. En caso de que no lo sean, lanzaremos una excepción ValueError. Consideremos el caso del ejemplo anterior: si el precio que se nos pasa por parámetro es negativo, como un precio siempre debe ser positivo, lanzaremos una excepción ValueError.

Una vez que se ejecuta una sentencia raise, termina la ejecución de la función, por lo que no se ejecutan más sentencias de la función. Si en el programa principal no se captura dicha excepción cuando se hace la llamada a la función, el programa terminará indicando el error cometido si se llega a lanzar la excepción.

Por lo tanto, cuando se realiza una función que lanza algún tipo de excepción, debe indicarse en los comentarios y documentación de la función.

Si no queremos indicar ninguna excepción particular, podemos utilizar la excepción Exception.

Ejemplo: while, break y try

Este modo de escribir bucles while es habitual, ya que así se puede comprobar la condición en cualquier punto del bucle (no solo al principio), y se puede expresar la condición de parada afirmativamente ("detente cuando ocurra...") en vez de tener que hacerlo con la lógica negativa ("sigue haciéndolo hasta que ocurra...").

La sentencia break podemos utilizarla cuando pedimos un número al usuario y este no lo ingresa.

while True:
try:
numero_str = input("Introduce un número: ")
numero = int(numero_str)
break
except:
print("Se produjo un error, el valor introducido no es un número.")
print(numero)