1. Code
  2. Python

Manejo profesional de errores con Python

Scroll to top

Spanish (Español) translation by Andrea Jiménez (you can also view the original English article)

En este tutorial, aprenderás a manejar las condiciones de error en Python desde el punto de vista del sistema completo. El manejo de errores es un aspecto fundamental del diseño, y va desde los niveles más bajos (a veces el hardware) hasta los usuarios finales. Si no tienes una estrategia coherente, tu sistema no será confiable, la experiencia del usuario será deficiente y tendrás muchos desafíos para depurar y solucionar problemas.

La clave del éxito es ser consciente de todos estos aspectos entrelazados, considerándolos explícitamente y formando una solución que resuelva cada punto.

Códigos de estado vs. Excepciones

Hay dos modelos principales de manejo de errores: códigos de estado y excepciones. Los códigos de estado pueden ser utilizados por cualquier lenguaje de programación. Las excepciones requieren compatibilidad con el lenguaje y el tiempo de ejecución.

Python admite excepciones. Python y su biblioteca estándar usan excepciones libremente para informar sobre muchas situaciones excepcionales como errores de E/S, dividir por cero, indexar fuera de los límites y también algunas situaciones no tan excepcionales como el final de la iteración (aunque está oculta). La mayoría de las bibliotecas hacen lo mismo y generan excepciones.

Esto significa que tu código tendrá que controlar las excepciones generadas por Python y las bibliotecas de todos modos, por lo que también puede generar excepciones de tu código cuando sea necesario y no depender de los códigos de estado.

Ejemplo rápido

Antes de profundizar en el lugar sagrado de las excepciones de Python y las mejores prácticas de manejo de errores, veamos algunos manejos de excepciones en acción:

1
def f():
2
3
    return 4 / 0
4
5
6
7
def g():
8
9
    raise Exception("Don't call us. We'll call you")
10
11
12
13
def h():
14
15
    try:
16
17
        f()
18
19
    except Exception as e:
20
21
        print(e)
22
23
    try:
24
25
        g()
26
27
    except Exception as e:
28
29
        print(e)

Esta es la salida al llamar a h():

1
h()
2
3
division by zero
4
5
Don't call us. We'll call you

Excepciones de Python

Las excepciones de Python son objetos organizados en una jerarquía de clases.

Esta es toda la jerarquía:

1
BaseException
2
3
 +-- SystemExit
4
5
 +-- KeyboardInterrupt
6
7
 +-- GeneratorExit
8
9
 +-- Exception
10
11
      +-- StopIteration
12
13
      +-- StandardError
14
15
      |    +-- BufferError
16
17
      |    +-- ArithmeticError
18
19
      |    |    +-- FloatingPointError
20
21
      |    |    +-- OverflowError
22
23
      |    |    +-- ZeroDivisionError
24
25
      |    +-- AssertionError
26
27
      |    +-- AttributeError
28
29
      |    +-- EnvironmentError
30
31
      |    |    +-- IOError
32
33
      |    |    +-- OSError
34
35
      |    |         +-- WindowsError (Windows)
36
37
      |    |         +-- VMSError (VMS)
38
39
      |    +-- EOFError
40
41
      |    +-- ImportError
42
43
      |    +-- LookupError
44
45
      |    |    +-- IndexError
46
47
      |    |    +-- KeyError
48
49
      |    +-- MemoryError
50
51
      |    +-- NameError
52
53
      |    |    +-- UnboundLocalError
54
55
      |    +-- ReferenceError
56
57
      |    +-- RuntimeError
58
59
      |    |    +-- NotImplementedError
60
61
      |    +-- SyntaxError
62
63
      |    |    +-- IndentationError
64
65
      |    |         +-- TabError
66
67
      |    +-- SystemError
68
69
      |    +-- TypeError
70
71
      |    +-- ValueError
72
73
      |         +-- UnicodeError
74
75
      |              +-- UnicodeDecodeError
76
77
      |              +-- UnicodeEncodeError
78
79
      |              +-- UnicodeTranslateError
80
81
      +-- Warning
82
83
           +-- DeprecationWarning
84
85
           +-- PendingDeprecationWarning
86
87
           +-- RuntimeWarning
88
89
           +-- SyntaxWarning
90
91
           +-- UserWarning
92
93
           +-- FutureWarning
94
95
  +-- ImportWarning
96
97
  +-- UnicodeWarning
98
99
  +-- BytesWarning
100
 

Hay varias excepciones especiales que se derivan directamente de BaseException, como SystemExit, KeyboardInterrupt y GeneratorExit. A continuación, está la clase Exception, que es la clase básica para StopIteration, StandardError y Warning. Todos los errores estándar se derivan de StandardError.

Cuando generas una excepción o alguna función que llamaste genera una excepción, ese flujo de código normal termina y la excepción comienza a propagarse por la pila de llamadas hasta que encuentra un controlador de excepciones adecuado. Si no hay ningún controlador de excepciones disponible para controlarlo, el proceso (o más exactamente el subproceso actual) terminará con un mensaje de excepción no controlado.

Generación de excepciones

Generar excepciones es muy fácil. Solo tienes que usar la palabra clave raise para generar un objeto que es una subclase de la clase Exception. Podría ser una instancia de Exception en sí, una de las excepciones estándar (por ejemplo, RuntimeError) o una subclase de Exception que tú mismo derivaste. Este es un pequeño fragmento de código que muestra todos los casos:

1
# Raise an instance of the Exception class itself

2
3
raise Exception('Ummm... something is wrong')
4
5
6
7
# Raise an instance of the RuntimeError class

8
9
raise RuntimeError('Ummm... something is wrong')
10
11
12
13
# Raise a custom subclass of Exception that keeps the timestamp the exception was created

14
15
from datetime import datetime
16
17
18
19
class SuperError(Exception):
20
21
    def __init__(self, message):
22
23
        Exception.__init__(message)
24
25
        self.when = datetime.now()
26
27
28
29
30
31
raise SuperError('Ummm... something is wrong')

Captura de excepciones

Detectas excepciones con la cláusula except, como viste en el ejemplo. Cuando detectas una excepción, tienes tres opciones:

  • Trágalo tranquilamente (manéjalo y sigue).
  • Haz algo como el registro, pero vuelve a generar la misma excepción para permitir que los niveles más altos se controlen.
  • Genera una excepción diferente en lugar del original.

Tragar la excepción

Debes tragarte la excepción si sabes cómo manejarla y puedes recuperarte por completo.

Por ejemplo, si recibes un archivo de entrada que puede estar en diferentes formatos (JSON, YAML), puedes intentar analizarlo con diferentes analizadores. Si el analizador JSON generó una excepción de que el archivo no es un archivo JSON válido, trágalo e intenta con el analizador YAML. Si el analizador YAML también falló, deja que la excepción se propague.

1
import json
2
3
import yaml
4
5
6
7
def parse_file(filename):
8
9
    try:
10
11
        return json.load(open(filename))
12
13
    except json.JSONDecodeError
14
15
        return yaml.load(open(filename))

Ten en cuenta que otras excepciones (por ejemplo, el archivo no encontrado o sin permisos de lectura) se propagarán y no serán detectadas por la cláusula except específica. Esta es una buena política en este caso en el que quieres probar el análisis YAML solo si el análisis JSON falló debido a un problema de codificación JSON.

Si quieres manejar todas las excepciones, usa except Exception. Por ejemplo:

1
def print_exception_type(func, *args, **kwargs):
2
3
    try:
4
5
        return func(*args, **kwargs)
6
7
    except Exception as e:
8
9
        print type(e)

Ten en cuenta que al agregar as e, vinculas el objeto de excepción al nombre e disponible en tu cláusula except.

Volver a generar la misma excepción

Para volver a generar, simplemente agrega raise sin argumentos dentro de tu controlador. Esto te permite realizar un manejo local, pero aún permite que los niveles superiores también lo manejen. Aquí, la función invoke_function() imprime el tipo de excepción en la consola y luego vuelve a generar la excepción.

1
def invoke_function(func, *args, **kwargs):
2
3
    try:
4
5
        return func(*args, **kwargs)
6
7
    except Exception as e:
8
9
        print type(e)
10
11
        raise

Generar una excepción diferente

Hay varios casos en los que te gustaría plantear una excepción diferente. A veces, quieres agrupar varias excepciones diferentes de bajo nivel en una sola categoría que se maneja uniformemente mediante un código de nivel superior. En los casos de orden, debes transformar la excepción al nivel de usuario y proporcionar un contexto específico de la aplicación.

Cláusula finally

A veces quieres asegurarte de que se ejecute algún código de limpieza incluso si se generó una excepción en algún momento. Por ejemplo, es posible que tengas una conexión a la base de datos que quieras cerrar una vez que hayas terminado. Esta es la forma incorrecta de hacerlo:

1
def fetch_some_data():
2
3
    db = open_db_connection()
4
5
    query(db)
6
7
    close_db_Connection(db)

Si la función query() genera una excepción, la llamada a close_db_connection() nunca se ejecutará y la conexión de la base de datos permanecerá abierta. La cláusula finally siempre se ejecuta después de que se ejecuta el controlador de excepción try all. Así se hace correctamente:

1
def fetch_some_data():
2
3
    db = None
4
5
    try:
6
7
        db = open_db_connection()
8
9
        query(db)
10
11
    finally:
12
13
        if db is not None:
14
15
            close_db_connection(db)

La llamada a open_db_connection() no puede devolver una conexión ni generar una excepción en sí. En este caso no hay necesidad de cerrar la conexión de la base de datos.

Al usar finally, debes tener cuidado de no generar ninguna excepción allí porque ocultará la excepción original.

Administradores de contexto

Los administradores de contexto proporcionan otro mecanismo para ajustar recursos como archivos o conexiones de base de datos en un código de limpieza que se ejecuta automáticamente incluso cuando se generaron excepciones. En lugar de bloques de prueba, usa la instrucción with. Este es un ejemplo con un archivo:

1
def process_file(filename):
2
3
     with open(filename) as f:
4
5
        process(f.read())

Ahora, incluso si process() genera una excepción, el archivo se cerrará correctamente inmediatamente cuando se salga del alcance del bloque with, independientemente de si la excepción se manejó o no.

Registro

El registro es prácticamente un requisito en sistemas no triviales y de larga duración. Es especialmente útil en aplicaciones web donde puedes tratar todas las excepciones de forma genérica: simplemente registra la excepción y devuelve un mensaje de error a la persona que llama.

Al iniciar sesión, es útil registrar el tipo de excepción, el mensaje de error y el stacktrace. Toda esta información está disponible a través del objeto sys.exc_info, pero si utilizas el método logger.exception() en tu controlador de excepciones, el sistema de registro de Python extraerá toda la información relevante para ti.

Esta es la mejor práctica que recomiendo:

1
import logging
2
3
logger = logging.getLogger()
4
5
6
7
def f():
8
9
    try:
10
11
        flaky_func()
12
13
    except Exception:
14
15
        logger.exception()
16
17
        raise

Si sigues este patrón entonces (suponiendo que configuraste el registro correctamente) pase lo que pase, tendrás un registro bastante bueno en tus registros de lo que salió mal, y podrás solucionar el problema.

Si vuelves a generar, asegúrate de no registrar la misma excepción una y otra vez en diferentes niveles. Es un desperdicio, y podría confundirte y hacerte creer que se produjeron varias instancias del mismo problema, cuando en la práctica se registró una sola instancia varias veces.

La forma más sencilla de hacerlo es permitir que todas las excepciones se propaguen (a menos que se puedan manejar con confianza y tragar antes) y, luego, hacer el registro cerca del nivel superior de tu aplicación/sistema.

Sentry

El registro es una capacidad. La implementación más común es el uso de archivos de registro. Pero, para sistemas distribuidos a gran escala con cientos, miles o más servidores, esta no siempre es la mejor solución.

Para realizar un seguimiento de las excepciones en toda tu infraestructura, un servicio como sentry es muy útil. Centraliza todos los informes de excepciones y, además del stacktrace, agrega el estado de cada marco de la pila (el valor de las variables en el momento en que se generó la excepción). También proporciona una interfaz muy agradable con paneles, informes y formas de desglosar los mensajes por varios proyectos. Es de código abierto, por lo que puedes ejecutar tu propio servidor o suscribirte a la versión alojada.

Lidiando con el fracaso transitorio

Algunos errores son temporales, en particular cuando se trata de sistemas distribuidos. Un sistema que se enloquece a la primera señal de problemas no es muy útil.

Si tu código accede a algún sistema remoto que no responde, la solución tradicional son los tiempos de espera, pero a veces no todos los sistemas están diseñados con tiempos de espera. Los tiempos de espera no siempre son fáciles de ajustar a medida que cambian las condiciones.

Otro enfoque es fallar rápidamente y luego volver a intentarlo. El beneficio es que si el objetivo está respondiendo rápido, entonces no tienes que pasar mucho tiempo en condiciones de sueño y puedes reaccionar inmediatamente. Pero si se produce un error, puedes reintentarlo varias veces hasta que decidas que es realmente inalcanzable y genera una excepción. En la siguiente sección, presentaré un decorador que puede hacerlo por ti.

Decoradores útiles

Dos decoradores que pueden ayudar con el manejo de errores son @log_error, que registra una excepción y luego la vuelve a generar, y el decorador @retry, que volverá a intentar llamar a una función varias veces.

Registrador de errores

Esta es una implementación simple. El decorador excepto un objeto registrador. Cuando decora una función y se invoca la función, ajustará la llamada en una cláusula try-except y, si hubo una excepción, la registrará y, finalmente, volverá a generar la excepción.

1
def log_error(logger)
2
3
    def decorated(f):
4
5
        @functools.wraps(f)
6
7
        def wrapped(*args, **kwargs):
8
9
            try:
10
11
                return f(*args, **kwargs)
12
13
            except Exception as e:
14
15
                if logger:
16
17
                    logger.exception(e)
18
19
                raise
20
21
        return wrapped
22
23
    return decorated

Así es como se usa:

1
import logging
2
3
logger = logging.getLogger()
4
5
6
7
@log_error(logger)
8
9
def f():
10
11
    raise Exception('I am exceptional')

Retrier

Esta es una muy buena implementación del decorador @retry.

1
import time
2
3
import math
4
5
6
7
# Retry decorator with exponential backoff

8
9
def retry(tries, delay=3, backoff=2):
10
11
  '''Retries a function or method until it returns True.

12


13


14


15
  delay sets the initial delay in seconds, and backoff sets the factor by which

16


17
  the delay should lengthen after each failure. backoff must be greater than 1,

18


19
  or else it isn't really a backoff. tries must be at least 0, and delay

20


21
  greater than 0.'''
22
23
24
25
  if backoff <= 1:
26
27
    raise ValueError("backoff must be greater than 1")
28
29
30
31
  tries = math.floor(tries)
32
33
  if tries < 0:
34
35
    raise ValueError("tries must be 0 or greater")
36
37
38
39
  if delay <= 0:
40
41
    raise ValueError("delay must be greater than 0")
42
43
44
45
  def deco_retry(f):
46
47
    def f_retry(*args, **kwargs):
48
49
      mtries, mdelay = tries, delay # make mutable

50
51
52
53
      rv = f(*args, **kwargs) # first attempt

54
55
      while mtries > 0:
56
57
        if rv is True: # Done on success

58
59
          return True
60
61
62
63
        mtries -= 1      # consume an attempt

64
65
        time.sleep(mdelay) # wait...

66
67
        mdelay *= backoff  # make future wait longer

68
69
70
71
        rv = f(*args, **kwargs) # Try again

72
73
74
75
      return False # Ran out of tries :-(

76
77
78
79
    return f_retry # true decorator -> decorated function

80
81
  return deco_retry  # @retry(arg[, ...]) -> true decorator

Conclusión

El manejo de errores es crucial tanto para los usuarios como para los desarrolladores. Python proporciona una gran compatibilidad en el lenguaje y la biblioteca estándar para el control de errores basado en excepciones. Si sigues las mejores prácticas cuidadosamente, puedes dominar este aspecto que suele ser ignorado.