Como utilizar y customizar nuestras propias excepciones con Python
¡Hola! ¡Que hay! Después de una larga temporada sin tocar Python toca volver y que mejor que con un tutorial para mejorar nuestros proyectos utilizando las excepciones con Python.
Pero bueno, antes de nada, para el que no lo tenga muy claro....
¿Qué son las excepciones?
Las excepciones son errores que se lanzan cuando ejecutamos un código en el que su sintaxis es correcta (Si hay errores de sintaxis, aparecerán primero ese tipo de errores). Por ejemplo, si realizamos una división entre 0 no nos aparecerá un error de sintaxis, pero al ejecutar el código veremos algo como esto:
num1 = 5
num2 = 0
x = num1 / num2
Traceback (most recent call last):
File ".\tutorial_exceptions.py", line 1, in <module>
x = 5 / 0
ZeroDivisionError: division by zero
Como podéis ver, al ejecutar nuestro script, Python lanzará un error y se parará el proceso. Ahora estamos en nuestra máquina local y oye, no pasa nada, lo arreglas y listo 🤷♂️. El problema es cuando tenemos estos fallos en producción que pueden romper el proceso y le damos una mala experiencia al usuario y nosotros no queremos eso.
¿Cómo utilizar las excepciones?
Bueno don't worry, hay solución. No podemos evitar los errores, pero si podemos capturarlos. Para ello utilizaremos el bloque try, except de la siguiente forma:
num1 = 5
num2 = 0
try:
x = num1 / num2
except:
print('Aquí ha ocurrido un error')
De esta forma intentará ejecutar el código dentro del bloque try y si hay algún error, lo capturará y se ejecutará el código contenido en el bloque except.
Vale esto esta muy bien, capturamos cualquier error que suceda en ese bloque de código, pero realmente no sabemos que error está ocurriendo (bueno en este caso si porque lo estamos forzando nosotros 😅). Aparte del error de división entre 0 puede haber más errores como por ejemplo que envíen un string u otro tipo de dato en vez de un entero y tengamos un error. Para resolver este problema podemos declarar el nombre de la excepción y especificar una variable con la información de error de esta forma:
num1 = 5
num2 = 0
try:
x = num1 / num2
except Exception as err:
print(err)
Al ejecutar este código la consola nos mostraría el siguiente mensaje:
division by zero
Esto está muy bien porque Exception nos capturará todas las excepciones que ocurran ya sea la excepción al dividir entre 0 o al enviar un string, o cualquiera que no tengamos contemplada, pero al hacerlo de esta forma aparecen dos problemas. El primero es que enviamos el mensaje tal cual al usuario y eso puede ser confuso y el segundo es que podemos estar enviando información comprometida al usuario sobre errores que no tengamos controlados.
Para evitar este tipo de problemas usaremos excepciones más concretas que solo se ejecutarán en según que casos. Por ejemplo podemos utilizar la excepción ZeroDivisionException que se ejecutará si el divisor es igual a 0, pero si por ejemplo envía un string no vamos a capturar ese error. Para utilizar este tipo de excepciones lo haremos de la siguiente forma:
num1 = '5'
num2 = 0
try:
x = num1 / num2
except ZeroDivisionError as err:
print('No puedes dividir un número entre 0')
except TypeError as err:
print('Los dos valores deben ser númericos')
except Exception as err:
print('Error desconocido')
En este caso estamos utilizando las excepciones ZeroDivisionError y TypeError, la primera se lanzará solo cuando el divisor sea 0 y la segunda solo si el tipo de dato es erróneo. Al final añadimos Exception general para capturar cualquier excepción que no tengamos contemplada y enviamos un mensaje al usuario.
Como tip, al lanzarse la excepción general podemos guardar un log enviándonos el tipo de excepción y la información enviada para seguir mejorando nuestra aplicación.
Por último, podemos utilizar el bloque finally al final de los bloques except. Este bloque se ejecutará siempre funcione el proceso o se lance una excepción:
num1 = '5'
num2 = 0
try:
x = num1 / num2
except ZeroDivisionError as err:
print('No puedes dividir un número entre 0')
except TypeError as err:
print('Los dos valores deben ser númericos')
except Exception as err:
params = {
'num1': num1,
'num2': num2
}
# Utilizamos la función type para saber que tipo de error ha ocurrido y name para obtener su nombre
save_log(type(err).__name__, params=params)
print('Error desconocido')
finally:
print('Fin del proceso')
# Aquí crearemos la lógica para guardar el log de error
def save_log(exception_name, params):
pass
Customizar excepciones
Crear nuestras propias excepciones es muy sencillo, solo tenemos que crear una clase y que esta extienda de la clase Exception:
class CustomException(Exception):
pass
Para probarlo utilizaremos el siguiente código:
try:
raise CustomException
except CustomException as err:
print(err)
Aquí hay dos cosas interesantes, la primera es la declaración raise la cual nos permitirá forzar el lanzamiento de una excepción. De esta forma podremos lanzar excepciones en los bloques de código en los cuales queramos parar un proceso cuando no se cumplan ciertas condiciones.
Si ejecutáis el código, veréis que err no está imprimiendo nada. Eso es porque no hemos definido ningún valor tanto en la clase como en la declaración. Una forma de solucionar esto sería así:
try:
raise CustomException('Hola', 'esto es un error')
except CustomException as err:
print(err)
De esta forma, al pasar los mensajes por argumentos al declarar la excepción, los podremos ver al imprimir el error. Lo bueno de esto es que podremos añadir todos los argumentos que queramos.
También podremos acceder a estos elementos a través de la instancia mediante el atributo args, el cual guarda una tupla con todos los argumentos pasados en la declaración:
try:
raise CustomException('Hola', 'esto es un error')
except CustomException as err:
print(err.args)
Por último, podremos añadir atributos a nuestras excepciones custom de forma que luego podamos acceder a ellos de forma sencilla al lanzar una excepción. Solo tenemos que modificar el constructor de nuestra excepción custom y declarar los atributos que queramos:
class MyHttpException(Exception):
def __init__(self, code=500, message='Internal server error'):
self.code = code
self.message = message
try:
raise MyHttpException
except MyHttpException as err:
print(err.code)
print(err.message)
try:
raise MyHttpException(code=404, message='Not found')
except MyHttpException as err:
print(err.code)
print(err.message)
Como veis, en este ejemplo he creado una excepción para capturar errores http. En el constructor he creado un parámetro code y otro message con unos valores por defecto. Después asigno esos parámetros a los atributos de la clase y al lanzar el primer bloque try podremos recuperar el mensaje y código por defecto.
En el segundo bloque los he modificado en la declaración para que podáis ver que pueden ser modificados sin problema. De esta forma podremos trabajar con nuestras excepciones customizadas, pero siempre con un cierto nivel de flexibilidad lo que nos ayudará a dar robustez a nuestras apps y poder informar al usuario y a nosotros mismos de la mejor forma posible.
Y bueno, esto es todo por este tutorial, creo que nos hemos metido bastante a fondo, pero si tenéis alguna duda recordad que tenéis la caja de comentarios para preguntar lo que sea que yo os intentaré ayudar 💪.
Espero que este post te ayude y como siempre, te recomiendo seguirme en Twitter para estar al tanto de los nuevo contenido. Ahora también puedes seguirme en Instagram donde estoy subiendo tips, tutoriales en vídeo e información sobre herramientas para developers.
Por último os dejo mi guía para aprender a trabajar con APIs donde explico todo el funcionamiento de una API, el protocolo HTTP y veremos como construir una API con arquitectura REST.
Nos leemos 👋.