Python & AI Tutorials Logo
Programación Python

25. Manejo elegante de excepciones

En el Capítulo 24, aprendimos a leer y entender las excepciones cuando ocurren. Ahora aprenderemos a manejar excepciones de forma elegante, permitiendo que nuestros programas se recuperen de errores en lugar de bloquearse. Esto es esencial para escribir programas robustos y fáciles de usar que puedan lidiar con situaciones inesperadas.

Cuando ocurre una excepción en Python, el flujo normal del programa se detiene inmediatamente. Pero ¿y si pudiéramos atrapar esa excepción antes de que bloquee nuestro programa? ¿Y si pudiéramos responder al error, quizá pidiéndole al usuario que lo intente de nuevo, o usando un valor predeterminado, o registrando el problema y continuando? Eso es exactamente lo que el manejo de excepciones nos permite hacer.

25.1) Usar bloques try y except

25.1.1) La estructura básica de un bloque try-except

Un bloque try-except es la forma de Python de decir: "intenta hacer esto y, si ocurre una excepción, haz esto otro en su lugar". La estructura básica se ve así:

python
try:
    # Código que podría lanzar una excepción
    risky_operation()
except:
    # Código que se ejecuta si ocurre CUALQUIER excepción
    print("Something went wrong!")

El bloque try contiene código que podría lanzar una excepción. Si ocurre una excepción en cualquier parte del bloque try, Python deja de ejecutar inmediatamente el bloque try y salta al bloque except. Si no ocurre ninguna excepción, el bloque except se omite por completo.

Veamos un ejemplo concreto. Recuerda del Capítulo 24 que intentar convertir una cadena inválida a un entero lanza un ValueError:

python
# Sin manejo de excepciones: el programa se bloquea
user_input = "hello"
number = int(user_input)  # ValueError: invalid literal for int() with base 10: 'hello'
print("This line never executes")

Ahora manejemos esta excepción de forma elegante:

python
# Con manejo de excepciones: el programa continúa
user_input = "hello"
 
try:
    number = int(user_input)
    print(f"You entered: {number}")
except:
    print("That's not a valid number!")
    number = 0  # Usar un valor predeterminado
 
print(f"Using number: {number}")

Output:

That's not a valid number!
Using number: 0

¡El programa no se bloqueó! Cuando int(user_input) lanzó un ValueError, Python saltó al bloque except, imprimió nuestro mensaje de error, estableció un valor predeterminado y luego continuó con el resto del programa.

Esto es lo que pasa paso a paso:

No

Iniciar bloque try

Ejecutar conversión a int

¿Se lanza una excepción?

Continuar en el bloque try

Saltar al bloque except

Omitir el bloque except

Ejecutar código del except

Continuar después de try-except

Comprender el "salto": qué pasa realmente

Cuando decimos que Python "salta" al bloque except, queremos decir que abandona la ejecución secuencial normal. Este es un cambio fundamental en cómo fluye tu programa, no solo una bifurcación simple como una sentencia if. Veámoslo en detalle con un ejemplo concreto:

python
# Observando el flujo de ejecución con excepciones
print("1. Starting program")
 
try:
    print("2. Entered try block")
    number = int("hello")  # La excepción sucede AQUÍ
    print("3. After conversion")   # Esta línea NUNCA se ejecuta
    result = number * 2            # Esta línea NUNCA se ejecuta
    print("4. After calculation")  # Esta línea NUNCA se ejecuta
except ValueError:
    print("5. In except block - handling the error")
 
print("6. After try-except block")

Output:

1. Starting program
2. Entered try block
5. In except block - handling the error
6. After try-except block

¡Observa que las líneas 3 y 4 nunca se ejecutan! En el momento en que int("hello") lanza un ValueError, Python:

  1. Detiene la ejecución del bloque try inmediatamente, justo en la línea donde ocurrió la excepción
  2. Busca una cláusula except coincidente que pueda manejar este tipo de excepción
  3. Salta a ese bloque except, omitiendo todo el código restante del bloque try
  4. Continúa después de la estructura try-except una vez que el bloque except termina

Esto es fundamentalmente diferente del flujo normal del programa. En una ejecución normal, Python ejecuta cada línea de forma secuencial. Con una excepción, Python abandona la ruta actual y toma un camino completamente distinto. Sin manejo de excepciones, el programa se bloquearía en la línea 2 y terminaría. Con manejo de excepciones, el programa se recupera y continúa.

Por qué esto importa:

Entender este comportamiento de "salto" es crucial porque:

  • Cualquier código después de la excepción en el bloque try se omite: no puedes asumir que se ejecutaron líneas posteriores en el bloque try
  • Puede que las variables no se inicialicen si la excepción ocurre antes de su asignación
  • Necesitas planificar en qué estado está tu programa cuando se ejecuta el bloque except

25.1.2) Manejar la entrada del usuario de forma segura

Uno de los usos más comunes del manejo de excepciones es validar la entrada del usuario. Los usuarios pueden escribir cualquier cosa, y necesitamos manejar las entradas inválidas de forma elegante. Aquí tienes un ejemplo práctico de un programa que pide la edad de un usuario:

python
# Entrada segura de edad con manejo de excepciones
print("Please enter your age:")
user_input = input()
 
try:
    age = int(user_input)
    print(f"You are {age} years old.")
    
    # Calcular el año de nacimiento (suponiendo que el año actual es 2024)
    birth_year = 2024 - age
    print(f"You were born around {birth_year}.")
except:
    print("Invalid input! Age must be a number.")
    print("Using default age of 0.")
    age = 0

Si el usuario introduce "25", la salida es:

Please enter your age:
25
You are 25 years old.
You were born around 1999.

Si el usuario introduce "twenty-five", la salida es:

Please enter your age:
twenty-five
Invalid input! Age must be a number.
Using default age of 0.

Observa cómo el programa maneja el error de forma elegante en lugar de bloquearse con un traceback. Esto es mucho mejor para la experiencia del usuario.

25.1.3) Manejar múltiples operaciones en un bloque try

Puedes poner múltiples operaciones en un solo bloque try. Si cualquiera de ellas lanza una excepción, Python salta al bloque except inmediatamente. Empecemos con un ejemplo simple:

python
# Dos operaciones en el bloque try
print("Enter a number:")
user_input = input()
 
try:
    number = int(user_input)      # Primera operación: podría lanzar ValueError
    result = 100 / number          # Segunda operación: podría lanzar ZeroDivisionError
    print(f"100 / {number} = {result}")
except:
    print("Something went wrong!")

Si el usuario introduce "hello", la excepción ocurre en la primera operación (conversión). Si el usuario introduce "0", la excepción ocurre en la segunda operación (división). En cualquier caso, nuestro único bloque except la captura.

Ahora ampliemos esto a tres operaciones:

python
# Múltiples operaciones en el bloque try
print("Enter two numbers to divide:")
numerator_input = input("Numerator: ")
denominator_input = input("Denominator: ")
 
try:
    numerator = int(numerator_input)      # Podría lanzar ValueError
    denominator = int(denominator_input)  # Podría lanzar ValueError
    result = numerator / denominator      # Podría lanzar ZeroDivisionError
    print(f"{numerator} / {denominator} = {result}")
except:
    print("Something went wrong with the calculation!")
    print("Make sure you enter valid numbers and don't divide by zero.")

Si el usuario introduce "10" y "2":

Enter two numbers to divide:
Numerator: 10
Denominator: 2
10 / 2 = 5.0

Si el usuario introduce "10" y "zero":

Enter two numbers to divide:
Numerator: 10
Denominator: zero
Something went wrong with the calculation!
Make sure you enter valid numbers and don't divide by zero.

Si el usuario introduce "10" y "0":

Enter two numbers to divide:
Numerator: 10
Denominator: 0
Something went wrong with the calculation!
Make sure you enter valid numbers and don't divide by zero.

En este ejemplo, tres cosas diferentes podrían salir mal: podría fallar la conversión del numerador, podría fallar la conversión del denominador, o podría fallar la división (si el denominador es cero). Nuestro único bloque except captura todos estos casos. Sin embargo, este enfoque tiene una limitación: no podemos saber qué error específico ocurrió. Abordaremos esto en la siguiente sección.

25.1.4) El problema con las cláusulas except vacías

Usar except: sin especificar un tipo de excepción se llama una cláusula except vacía (bare except clause). Aunque captura todas las excepciones, a menudo es demasiado amplio y puede ocultar problemas inesperados. Considera este ejemplo:

python
# Una cláusula except vacía captura TODO, incluso cosas que no esperamos
numbers = [10, 20, 30]
 
try:
    index = 5  # Esperamos IndexError si index está fuera de rango
    value = numbers[index]
    print(f"Value at index {index}: {value}")
except:
    print("Could not access the list element.")

Esto parece razonable: estamos intentando acceder a un elemento de una lista que podría no existir. Pero ¿y si hay un error tipográfico en nuestro código?

python
# ¿Y si hay un error tipográfico en nuestro código?
numbers = [10, 20, 30]
 
try:
    index = 2
    value = numbrs[index]  # Typo: 'numbrs' instead of 'numbers'
    print(f"Value at index {index}: {value}")
except:
    print("Could not access the list element.")

Output:

Could not access the list element.

La cláusula except vacía captura el NameError del error tipográfico e imprime "Could not access the list element", ¡dándonos información incorrecta sobre qué salió mal! Pensamos que el índice está fuera de rango, pero en realidad tenemos un error tipográfico en el nombre de nuestra variable.

Una cláusula except vacía también captura KeyboardInterrupt (cuando el usuario pulsa Ctrl+C) y SystemExit (cuando el programa intenta salir), que por lo general no deberían capturarse. Por estas razones, es mejor capturar excepciones específicas, que es lo que aprenderemos a continuación.

25.2) Capturar excepciones específicas

25.2.1) Especificar tipos de excepción

En lugar de capturar todas las excepciones con un except vacío, podemos especificar qué tipos de excepción queremos manejar. Esto hace nuestro código más preciso y nos ayuda a responder adecuadamente a diferentes errores:

python
# Capturando un tipo de excepción específico
user_input = "hello"
 
try:
    number = int(user_input)
    print(f"You entered: {number}")
except ValueError:
    print("That's not a valid number!")
    number = 0
 
print(f"Using number: {number}")

Output:

That's not a valid number!
Using number: 0

Ahora nuestra cláusula except solo captura excepciones ValueError. Si ocurre un tipo diferente de excepción (como un NameError por un error tipográfico), no se capturará y veremos el traceback completo, ¡lo cual en realidad es útil para la depuración!

La sintaxis es: except ExceptionType: donde ExceptionType es el nombre de la clase de excepción que quieres capturar (como ValueError, TypeError, ZeroDivisionError, etc.).

Error común: capturar el tipo de excepción incorrecto

¿Qué pasa si especificas un tipo de excepción que no coincide con lo que realmente ocurre? Veamos:

python
# Capturando el tipo de excepción incorrecto
user_input = "hello"
 
try:
    number = int(user_input)  # Esto lanza ValueError
    print(f"You entered: {number}")
except TypeError:  # ¡Pero estamos capturando TypeError!
    print("That's not a valid number!")
    number = 0
 
print(f"Using number: {number}")

Output:

Traceback (most recent call last):
  File "example.py", line 4, in <module>
    number = int(user_input)
ValueError: invalid literal for int() with base 10: 'hello'

¡El programa se bloqueó! ¿Por qué? Porque int("hello") lanza un ValueError, pero nuestra cláusula except solo captura TypeError. Como no hay una cláusula except que coincida, la excepción no se captura y el programa termina.

Esto en realidad ayuda durante el desarrollo: si capturas el tipo de excepción incorrecto, verás el traceback completo y te darás cuenta de tu error. Esta es una de las razones por las que capturar excepciones específicas es mejor que usar except vacío.

Cómo evitar este error:

  1. Lee el traceback para ver qué tipo de excepción ocurrió realmente
  2. Usa ese tipo de excepción específico en tu cláusula except
  3. Si no estás seguro, ejecuta el código y deja que se bloquee: ¡el traceback te lo dirá!

25.2.2) Manejar diferentes excepciones de manera diferente

Puedes tener múltiples cláusulas except para manejar diferentes tipos de excepción de diferentes maneras. Esto es extremadamente útil cuando diferentes errores requieren diferentes respuestas:

python
# Manejo diferente para diferentes excepciones
print("Enter two numbers to divide:")
numerator_input = input("Numerator: ")
denominator_input = input("Denominator: ")
 
try:
    numerator = int(numerator_input)
    denominator = int(denominator_input)
    result = numerator / denominator
    print(f"{numerator} / {denominator} = {result}")
except ValueError:
    print("Error: Both inputs must be valid integers.")
    print("You entered something that isn't a number.")
except ZeroDivisionError:
    print("Error: Cannot divide by zero.")
    print("The denominator must be a non-zero number.")

Si el usuario introduce "10" y "abc":

Enter two numbers to divide:
Numerator: 10
Denominator: abc
Error: Both inputs must be valid integers.
You entered something that isn't a number.

Si el usuario introduce "10" y "0":

Enter two numbers to divide:
Numerator: 10
Denominator: 0
Error: Cannot divide by zero.
The denominator must be a non-zero number.

Python revisa cada cláusula except en orden. Cuando ocurre una excepción, Python encuentra la primera cláusula except que coincide con el tipo de excepción y ejecuta ese bloque. Las otras cláusulas except se omiten.

No

No

No

Excepción lanzada en el bloque try

¿Coincide con el primer except?

Ejecutar el primer bloque except

¿Coincide con el segundo except?

Ejecutar el segundo bloque except

¿Coincide con el tercer except?

Ejecutar el tercer bloque except

Excepción no capturada: se propaga hacia arriba

Continuar después de try-except

25.2.3) Capturar múltiples tipos de excepción en una sola cláusula

A veces quieres manejar varios tipos de excepción diferentes de la misma manera. En lugar de escribir varios bloques except idénticos, puedes capturar múltiples tipos de excepción en una sola cláusula poniéndolos entre paréntesis como una tupla:

python
# Capturando múltiples tipos de excepción juntos
print("Enter a number:")
user_input = input()
 
try:
    number = int(user_input)
    result = 100 / number
    print(f"100 divided by {number} is {result}")
except (ValueError, ZeroDivisionError):
    print("Invalid input or division by zero.")
    print("Please enter a non-zero number.")

Si el usuario introduce "hello":

Enter a number:
hello
Invalid input or division by zero.
Please enter a non-zero number.

Si el usuario introduce "0":

Enter a number:
0
Invalid input or division by zero.
Please enter a non-zero number.

Tanto ValueError (por conversión inválida) como ZeroDivisionError (por dividir por cero) se manejan con la misma cláusula except. Esto es útil cuando diferentes errores deberían desencadenar la misma respuesta.

25.2.4) Acceder a la información de la excepción

A veces necesitas conocer más detalles sobre la excepción que ocurrió. Puedes capturar el objeto de la excepción usando la palabra clave as. Pero primero, entendamos qué es realmente un objeto de excepción.

¿Qué es un objeto de excepción?

Cuando Python lanza una excepción, no solo señala que algo salió mal: crea un objeto que contiene información sobre el error. Este objeto de excepción es como un informe de error detallado que incluye:

  • El mensaje de error: una descripción de lo que salió mal
  • El tipo de excepción: qué clase de error ocurrió (ValueError, TypeError, etc.)
  • Atributos adicionales: información específica dependiendo del tipo de excepción

Piensa en un objeto de excepción como un contenedor que guarda toda la información sobre un error. Igual que un objeto lista contiene elementos y tiene métodos como append(), un objeto de excepción contiene información del error y tiene atributos a los que puedes acceder.

Cuando escribes except ValueError as error:, le estás diciendo a Python: "Si ocurre un ValueError, crea una variable llamada error y mete el objeto de excepción ahí para que pueda examinarlo."

Exploremos qué hay dentro de un objeto de excepción:

python
# Examinando el contenido del objeto de excepción
try:
    number = int("hello")
except ValueError as error:
    print("Exception caught! Let's examine it:")
    print(f"Type: {type(error)}")
    print(f"String representation: {error}")
    print(f"Args tuple: {error.args}")

Output:

Exception caught! Let's examine it:
Type: <class 'ValueError'>
String representation: invalid literal for int() with base 10: 'hello'
Args tuple: ("invalid literal for int() with base 10: 'hello'",)

El objeto de excepción tiene:

  • Un tipo (clase ValueError): esto te dice qué clase de error ocurrió
  • Una representación en cadena (el mensaje de error): esto es lo que ves en los tracebacks
  • Un atributo args (tupla que contiene el mensaje y cualquier otro argumento): esto proporciona acceso estructurado a los detalles del error

Por qué esto importa:

Diferentes tipos de excepción tienen diferentes atributos que aportan información específica. Entender la estructura de los objetos de excepción te ayuda a extraer información útil para depuración o para feedback al usuario:

python
# Diferentes excepciones tienen diferentes atributos
numbers = [10, 20, 30]
 
try:
    value = numbers[10]
except IndexError as error:
    print(f"IndexError message: {error}")
    print(f"Exception args: {error.args}")
 
# Ahora prueba con un diccionario
grades = {"Alice": 95}
 
try:
    grade = grades["Bob"]
except KeyError as error:
    print(f"KeyError message: {error}")
    print(f"Missing key: {error.args[0]}")

Output:

IndexError message: list index out of range
Exception args: ('list index out of range',)
KeyError message: 'Bob'
Missing key: Bob

Observa cómo KeyError incluye la clave real que faltaba en su mensaje. Diferentes tipos de excepción proporcionan distinta información útil a la que puedes acceder mediante el objeto de excepción.

25.3) Usar else y finally con bloques try

25.3.1) La cláusula else: código que se ejecuta solo si hay éxito

La cláusula else en un bloque try-except se ejecuta solo si no ocurrió ninguna excepción en el bloque try. Esto es útil para código que solo debería ejecutarse cuando la operación riesgosa tiene éxito:

python
# Usando else para código que solo corre en caso de éxito
print("Enter a number:")
user_input = input()
 
try:
    number = int(user_input)
except ValueError:
    print("That's not a valid number!")
else:
    # Esto solo se ejecuta si int(user_input) tuvo éxito
    print(f"Successfully converted: {number}")
    squared = number ** 2
    print(f"The square of {number} is {squared}")

Si el usuario introduce "5":

Enter a number:
5
Successfully converted: 5
The square of 5 is 25

Si el usuario introduce "hello":

Enter a number:
hello
That's not a valid number!

¿Por qué usar else en lugar de simplemente poner el código al final del bloque try? Hay dos razones importantes:

  1. Claridad: la cláusula else deja explícito que este código solo se ejecuta en caso de éxito
  2. Alcance de la excepción: las excepciones lanzadas en la cláusula else no son capturadas por las cláusulas except anteriores

Aquí tienes un ejemplo que muestra por qué el segundo punto importa:

python
# Demostrando por qué else es útil para el alcance de la excepción
try:
    number_1 = int(input("Enter a number_1: "))
except ValueError:
    print("Invalid input!")
else:
    # Si ocurre un error aquí, no será capturado por el except anterior
    # Esto ayuda a distinguir entre errores de entrada y errores de procesamiento
    number_2 = int(input("Enter a number_2: ")) # Podría lanzar ValueError

Si ponemos number_2 = int(input(...)) en el bloque try junto con number_1, cualquier ValueError de cualquiera de las entradas sería capturado por la misma cláusula except ValueError. Esto hace imposible saber qué entrada causó el problema.

Al poner number_2 = int(input(...)) en el bloque else, separamos el manejo de errores. La cláusula except solo captura errores de number_1, mientras que los errores de number_2 lanzarán una excepción no capturada con un traceback completo, dejando claro que falló la segunda entrada, no la primera.

25.3.2) La cláusula finally: código que siempre se ejecuta

La cláusula finally contiene código que se ejecuta pase lo que pase: ocurra una excepción o no, se capture o no. Esto es esencial para operaciones de limpieza que deben ocurrir siempre:

python
# Usando finally para limpieza
print("Enter a number:")
user_input = input()
 
try:
    number = int(user_input)
    result = 100 / number
    print(f"Result: {result}")
except ValueError:
    print("Invalid number!")
except ZeroDivisionError:
    print("Cannot divide by zero!")
finally:
    print("Calculation attempt completed.")

Si el usuario introduce "5":

Enter a number:
5
Result: 20.0
Calculation attempt completed.

Si el usuario introduce "hello":

Enter a number:
hello
Invalid number!
Calculation attempt completed.

Si el usuario introduce "0":

Enter a number:
0
Cannot divide by zero!
Calculation attempt completed.

¡El bloque finally se ejecuta en los tres casos! Este es el comportamiento clave de finally: se ejecuta siempre, sin importar lo que pasó en el bloque try.

No

No

No

Iniciar bloque try

¿Se lanza una excepción?

Completar el bloque try

¿Se captura la excepción?

Ejecutar bloque else si está presente

Ejecutar el bloque except coincidente

La excepción se propaga

Ejecutar el bloque finally

¿Se capturó la excepción?

Continuar después de try-except-finally

La excepción sigue propagándose

25.3.3) Combinar try, except, else y finally

Puedes usar las cuatro cláusulas juntas para crear un manejo de excepciones integral:

python
# Estructura completa de manejo de excepciones
print("Enter a number to calculate its reciprocal:")
user_input = input()
 
try:
    # Operaciones arriesgadas
    number = int(user_input)
    reciprocal = 1 / number
except ValueError:
    # Manejar errores de conversión
    print("Error: Input must be a valid integer.")
except ZeroDivisionError:
    # Manejar división por cero
    print("Error: Cannot calculate reciprocal of zero.")
else:
    # Código solo para el caso de éxito
    print(f"The reciprocal of {number} is {reciprocal}")
    print(f"Verification: {number} × {reciprocal} = {number * reciprocal}")
finally:
    # Código de limpieza que siempre se ejecuta
    print("Reciprocal calculation completed.")

Si el usuario introduce "4":

Enter a number to calculate its reciprocal:
4
The reciprocal of 4 is 0.25
Verification: 4 × 0.25 = 1.0
Reciprocal calculation completed.

Si el usuario introduce "hello":

Enter a number to calculate its reciprocal:
hello
Error: Input must be a valid integer.
Reciprocal calculation completed.

Si el usuario introduce "0":

Enter a number to calculate its reciprocal:
0
Error: Cannot calculate reciprocal of zero.
Reciprocal calculation completed.

El flujo de ejecución es:

  1. El bloque try siempre se ejecuta primero
  2. Si ocurre una excepción, se ejecuta el bloque except coincidente
  3. Si no ocurre ninguna excepción, se ejecuta el bloque else (si está presente)
  4. El bloque finally siempre se ejecuta al final, sin importar lo que haya pasado

25.4) Lanzar excepciones deliberadamente con raise

25.4.1) ¿Por qué lanzar excepciones?

Hasta ahora, hemos estado capturando excepciones que Python lanza automáticamente. Pero a veces necesitas lanzar una excepción deliberadamente en tu propio código. Esto es útil cuando:

  1. Detectas una situación inválida que tu código no puede manejar
  2. Quieres hacer cumplir reglas o restricciones
  3. Quieres señalar un error al código que llamó a tu función

Lanzar una excepción es la forma de Python de decir: "No puedo continuar: algo está mal, y quien me llamó necesita lidiar con ello."

La sintaxis es sencilla: raise ExceptionType("error message")

Aquí tienes un ejemplo básico:

python
# Lanzando una excepción deliberadamente
age = -5
 
if age < 0:
    raise ValueError("Age cannot be negative!")
 
print(f"Age: {age}")  # Esta línea nunca se ejecuta

Output:

Traceback (most recent call last):
  File "example.py", line 5, in <module>
    raise ValueError("Age cannot be negative!")
ValueError: Age cannot be negative!

Cuando Python encuentra raise, crea inmediatamente una excepción y empieza a buscar un bloque except que la maneje. Si no existe ninguno, el programa termina con un traceback.

25.4.2) Lanzar excepciones en funciones

Lanzar excepciones es particularmente útil en funciones para validar entradas y hacer cumplir restricciones:

python
# Función que valida la entrada lanzando excepciones
def calculate_discount(price, discount_percent):
    """Calculate discounted price.
    
    Args:
        price: Original price (must be positive)
        discount_percent: Discount percentage (must be 0-100)
    
    Returns:
        Discounted price
    
    Raises:
        ValueError: If inputs are invalid
    """
    if price < 0:
        raise ValueError("Price cannot be negative!")
    
    if discount_percent < 0 or discount_percent > 100:
        raise ValueError("Discount must be between 0 and 100!")
    
    discount_amount = price * (discount_percent / 100)
    return price - discount_amount
 
# Usando la función
try:
    final_price = calculate_discount(100, 20)
    print(f"Final price: ${final_price}")
except ValueError as error:
    print(f"Error: {error}")

Output:

Final price: $80.0

Ahora probemos con entradas inválidas:

python
# Precio inválido
try:
    final_price = calculate_discount(-50, 20)
    print(f"Final price: ${final_price}")
except ValueError as error:
    print(f"Error: {error}")

Output:

Error: Price cannot be negative!
python
# Descuento inválido
try:
    final_price = calculate_discount(100, 150)
    print(f"Final price: ${final_price}")
except ValueError as error:
    print(f"Error: {error}")

Output:

Error: Discount must be between 0 and 100!

Al lanzar excepciones, la función comunica claramente qué salió mal. El código que llama puede entonces decidir cómo manejar el error: quizá pidiéndole al usuario una nueva entrada, usando valores predeterminados o registrando el error.

25.4.3) Elegir el tipo de excepción correcto

Python tiene muchos tipos de excepción integrados, y elegir el correcto hace que tu código sea más claro. Aquí están las excepciones más usadas para validación:

  • ValueError: úsala cuando un valor tiene el tipo correcto pero un valor inapropiado (p. ej., edad negativa, porcentaje inválido)
  • TypeError: úsala cuando un valor es completamente del tipo incorrecto (p. ej., una cadena en lugar de un número)
  • KeyError: úsala cuando una clave de diccionario no existe
  • IndexError: úsala cuando un índice de una secuencia está fuera de rango

Aquí tienes un ejemplo mostrando diferentes tipos de excepción:

python
# Usando tipos de excepción apropiados
def get_student_grade(grades, student_name):
    """Get a student's grade from the grades dictionary.
    
    Args:
        grades: Dictionary mapping student names to grades
        student_name: Name of the student
    
    Returns:
        The student's grade
    
    Raises:
        TypeError: If grades is not a dictionary
        KeyError: If student_name is not in grades
        ValueError: If the grade is invalid
    """
    if not isinstance(grades, dict):
        raise TypeError("Grades must be a dictionary!")
    
    if student_name not in grades:
        raise KeyError(f"Student '{student_name}' not found!")
    
    grade = grades[student_name]
    
    if not (0 <= grade <= 100):
        raise ValueError(f"Invalid grade: {grade} (must be 0-100)")
    
    return grade
 
# Probar con datos válidos
grades = {"Alice": 95, "Bob": 87, "Carol": 92}
 
try:
    grade = get_student_grade(grades, "Alice")
    print(f"Alice's grade: {grade}")
except (TypeError, KeyError, ValueError) as error:
    print(f"Error: {error}")

Output:

Alice's grade: 95
python
# Probar con estudiante faltante
try:
    grade = get_student_grade(grades, "David")
    print(f"David's grade: {grade}")
except (TypeError, KeyError, ValueError) as error:
    print(f"Error: {error}")

Output:

Error: Student 'David' not found!
python
# Probar con tipo incorrecto
try:
    grade = get_student_grade("not a dict", "Alice")
    print(f"Alice's grade: {grade}")
except (TypeError, KeyError, ValueError) as error:
    print(f"Error: {error}")

Output:

Error: Grades must be a dictionary!

Usar el tipo de excepción apropiado ayuda a otros programadores (y a tu yo futuro) a entender qué tipo de error ocurrió.

25.4.4) Volver a lanzar excepciones

A veces quieres capturar una excepción, hacer algo (como registrar en logs) y luego dejar que la excepción siga propagándose. Puedes hacer esto usando raise sin ningún argumento dentro de un bloque except:

python
# Volver a lanzar una excepción después de registrar en logs
def divide_numbers(a, b):
    """Divide two numbers with error logging."""
    try:
        result = a / b
        return result
    except ZeroDivisionError:
        print("ERROR LOG: Division by zero attempted")
        print(f"  Numerator: {a}, Denominator: {b}")
        raise  # Volver a lanzar la misma excepción
 
# Usando la función
try:
    result = divide_numbers(10, 0)
    print(f"Result: {result}")
except ZeroDivisionError:
    print("Cannot divide by zero!")

Output:

ERROR LOG: Division by zero attempted
  Numerator: 10, Denominator: 0
Cannot divide by zero!

La sentencia raise sin argumentos vuelve a lanzar la excepción que acaba de capturarse. Esto es útil cuando quieres:

  1. Registrar o guardar el error
  2. Hacer alguna limpieza
  3. Dejar que el error se propague al llamador

25.4.5) Lanzar excepciones desde excepciones

A veces quieres lanzar una nueva excepción mientras manejas otra, preservando el contexto del error original. Python 3 proporciona la sintaxis raise ... from ... para esto:

python
# Lanzando una nueva excepción a partir de una existente
def load_config(config_dict, key):
    """Load configuration value from dictionary."""
    try:
        config_value = config_dict[key]
        
        # Intentar analizar como entero
        parsed_value = int(config_value)
        return parsed_value
        
    except KeyError as error:
        raise RuntimeError(f"Configuration key missing: {key}") from error
    except ValueError as error:
        raise RuntimeError(f"Invalid configuration format for {key}") from error
 
# Usando la función
config = {"timeout": "30", "retries": "5"}
 
try:
    value = load_config(config, "timeout")
    print(f"Config value: {value}")
except RuntimeError as error:
    print(f"Configuration error: {error}")
    print(f"Original cause: {error.__cause__}")

Output:

Config value: 30

Si la clave no existe:

python
try:
    value = load_config(config, "missing_key")
    print(f"Config value: {value}")
except RuntimeError as error:
    print(f"Configuration error: {error}")
    print(f"Original cause: {error.__cause__}")

Output:

Configuration error: Configuration key missing: missing_key
Original cause: 'missing_key'

La palabra clave from enlaza la nueva excepción con la original. Esto crea una cadena de excepciones que ayuda con la depuración: puedes ver tanto lo que salió mal a un alto nivel (error de configuración) como cuál fue la causa subyacente (clave no encontrada).


El manejo de excepciones es una de las herramientas más importantes para escribir programas confiables. Al usar bloques try-except, puedes anticipar problemas, manejarlos de forma elegante y ofrecer una mejor experiencia a tus usuarios. Recuerda:

  • Usa try-except para manejar errores esperados de forma elegante
  • Captura tipos de excepción específicos en lugar de usar except vacío
  • Usa else para código que solo debería ejecutarse si hay éxito
  • Usa finally para código de limpieza que debe ejecutarse siempre
  • Lanza excepciones en tu propio código para señalar problemas
  • Elige tipos de excepción apropiados para que los errores sean claros
  • Proporciona mensajes de error útiles que expliquen qué salió mal

En el próximo capítulo, aprenderemos técnicas de programación defensiva que combinan el manejo de excepciones con validación de entrada y otras estrategias para hacer nuestros programas aún más robustos.

© 2025. Primesoft Co., Ltd.
support@primesoft.ai