Manejo de excepciones
Aprende a controlar errores y situaciones inesperadas en tus programas Python mediante el manejo de excepciones, mejorando la robustez y fiabilidad de tu código.
Cristian Escalante
Última actualización: 21 de mayo de 2025
Manejo de excepciones en Python
En programación, los errores son inevitables. Incluso en el código mejor escrito pueden surgir situaciones inesperadas: un archivo que no existe, una conexión de red que falla, o una entrada de usuario incorrecta. El manejo de excepciones es una técnica que permite a los programas detectar y responder a estos errores de manera controlada, en lugar de fallar abruptamente.
Python proporciona un mecanismo robusto para el manejo de excepciones que te permite escribir código más resiliente y confiable.
¿Qué son las excepciones?
Las excepciones son eventos que ocurren durante la ejecución de un programa y que interrumpen el flujo normal de las instrucciones. En Python, las excepciones son objetos que representan errores.
Cuando ocurre un error durante la ejecución, Python genera (o "lanza") una excepción. Si esta excepción no es manejada (o "capturada"), el programa se detiene y muestra un mensaje de error llamado "traceback".
Ejemplos de excepciones comunes
# ZeroDivisionError: División por cero
resultado = 10 / 0
# TypeError: Operación entre tipos incompatibles
resultado = "5" + 5
# NameError: Variable no definida
print(variable_inexistente)
# IndexError: Índice fuera de rango
lista = [1, 2, 3]
elemento = lista[10]
# KeyError: Clave no encontrada en un diccionario
diccionario = {"a": 1, "b": 2}
valor = diccionario["c"]
# FileNotFoundError: Archivo no encontrado
archivo = open("archivo_inexistente.txt", "r")
Estructura básica del manejo de excepciones
Python utiliza los bloques try
, except
, else
y finally
para manejar excepciones:
try:
# Código que podría generar una excepción
...
except TipoDeExcepcion:
# Código que se ejecuta si ocurre una excepción del tipo especificado
...
else:
# Código que se ejecuta si no ocurre ninguna excepción en el bloque try
...
finally:
# Código que se ejecuta siempre, haya o no ocurrido una excepción
...
Ejemplo básico
try:
numero = int(input("Introduce un número: "))
resultado = 10 / numero
print(f"El resultado es: {resultado}")
except ValueError:
print("Error: Debes introducir un número válido.")
except ZeroDivisionError:
print("Error: No puedes dividir entre cero.")
Capturando múltiples excepciones
Hay varias formas de manejar múltiples tipos de excepciones:
1. Múltiples bloques except
try:
# Código que podría generar excepciones
numero = int(input("Introduce un número: "))
resultado = 10 / numero
print(f"El resultado es: {resultado}")
except ValueError:
print("Error: Debes introducir un número válido.")
except ZeroDivisionError:
print("Error: No puedes dividir entre cero.")
2. Agrupar excepciones en un solo bloque except
try:
numero = int(input("Introduce un número: "))
resultado = 10 / numero
print(f"El resultado es: {resultado}")
except (ValueError, ZeroDivisionError):
print("Error: Entrada inválida o división por cero.")
3. Capturar cualquier excepción
try:
numero = int(input("Introduce un número: "))
resultado = 10 / numero
print(f"El resultado es: {resultado}")
except Exception as e:
print(f"Error inesperado: {e}")
Nota: Capturar
Exception
sin especificar un tipo concreto puede ocultar errores inesperados. Es generalmente mejor capturar tipos específicos de excepciones.
Bloques else y finally
Bloque else
El bloque else
se ejecuta solo si no ocurre ninguna excepción en el bloque try
:
try:
numero = int(input("Introduce un número: "))
resultado = 10 / numero
except ValueError:
print("Error: Debes introducir un número válido.")
except ZeroDivisionError:
print("Error: No puedes dividir entre cero.")
else:
print(f"El resultado es: {resultado}")
print("Operación completada con éxito.")
Bloque finally
El bloque finally
se ejecuta siempre, independientemente de si ocurre una excepción o no:
try:
archivo = open("datos.txt", "r")
contenido = archivo.read()
except FileNotFoundError:
print("Error: El archivo no existe.")
finally:
# Este bloque se ejecuta siempre
try:
archivo.close()
print("Archivo cerrado.")
except:
# En caso de que archivo no se haya abierto
pass
El bloque finally
es especialmente útil para liberar recursos (cerrar archivos, conexiones de red, etc.) que deben ser liberados independientemente de si ocurrió un error.
Obteniendo información sobre la excepción
Puedes obtener información detallada sobre la excepción usando la cláusula as
:
try:
numero = int(input("Introduce un número: "))
resultado = 10 / numero
except Exception as e:
print(f"Error: {e}")
print(f"Tipo de error: {type(e).__name__}")
Lanzando excepciones con raise
Además de manejar excepciones, puedes lanzar tus propias excepciones usando la instrucción raise
:
def dividir(a, b):
if b == 0:
raise ZeroDivisionError("No se puede dividir entre cero")
return a / b
try:
resultado = dividir(10, 0)
except ZeroDivisionError as e:
print(f"Error: {e}")
Re-lanzando excepciones
A veces, quieres capturar una excepción, hacer algo con ella, y luego volver a lanzarla para que sea manejada en un nivel superior:
def procesar_archivo(nombre_archivo):
try:
with open(nombre_archivo, "r") as archivo:
return archivo.read()
except FileNotFoundError:
print(f"Advertencia: El archivo {nombre_archivo} no existe.")
raise # Re-lanza la última excepción
Creando excepciones personalizadas
Puedes crear tus propias clases de excepciones heredando de la clase Exception
o de alguna de sus subclases:
class ErrorDeValidacion(Exception):
"""Excepción lanzada por errores en la validación de datos."""
pass
class EdadInvalidaError(ErrorDeValidacion):
"""Excepción lanzada cuando la edad está fuera del rango permitido."""
def __init__(self, edad, mensaje="La edad debe estar entre 0 y 120 años"):
self.edad = edad
self.mensaje = mensaje
super().__init__(self.mensaje)
def __str__(self):
return f"{self.mensaje}: {self.edad}"
def validar_edad(edad):
if not 0 <= edad <= 120:
raise EdadInvalidaError(edad)
return True
# Uso
try:
validar_edad(150)
except EdadInvalidaError as e:
print(f"Error: {e}")
Jerarquía de excepciones en Python
Python tiene una rica jerarquía de excepciones incorporadas. Todas las excepciones heredan de la clase base BaseException
, pero la mayoría de las excepciones que utilizarás heredan de Exception
.
Aquí hay una versión simplificada de la jerarquía:
BaseException
├── SystemExit # Lanzada por sys.exit()
├── KeyboardInterrupt # Lanzada cuando el usuario presiona Ctrl+C
├── GeneratorExit # Lanzada cuando un generador es cerrado
└── Exception # Base para la mayoría de excepciones
├── StopIteration # Lanzada para terminar la iteración
├── ArithmeticError # Base para errores aritméticos
│ ├── FloatingPointError
│ ├── OverflowError
│ └── ZeroDivisionError
├── AssertionError # Lanzada por la instrucción assert
├── AttributeError # Atributo no encontrado
├── EOFError # Fin de archivo inesperado
├── ImportError # Error al importar un módulo
│ └── ModuleNotFoundError
├── LookupError # Base para errores de búsqueda
│ ├── IndexError # Índice fuera de rango
│ └── KeyError # Clave no encontrada
├── NameError # Variable no definida
├── OSError # Error del sistema operativo
│ ├── FileExistsError
│ ├── FileNotFoundError
│ ├── PermissionError
│ └── ...
├── TypeError # Operación con tipos incompatibles
├── ValueError # Valor incorrecto
│ └── UnicodeError
└── ...
Conocer esta jerarquía te ayuda a decidir qué excepciones capturar y en qué orden.
Patrones comunes en el manejo de excepciones
1. EAFP vs LBYL
Python favorece el enfoque "Es más fácil pedir perdón que permiso" (EAFP, "Easier to Ask Forgiveness than Permission") sobre "Mira antes de saltar" (LBYL, "Look Before You Leap"):
# Enfoque LBYL (no preferido en Python)
if "clave" in diccionario:
valor = diccionario["clave"]
else:
valor = valor_predeterminado
# Enfoque EAFP (preferido en Python)
try:
valor = diccionario["clave"]
except KeyError:
valor = valor_predeterminado
2. Limpieza de recursos con context managers
El patrón más común para manejar recursos que necesitan ser liberados es usar un context manager con la instrucción with
:
# Sin context manager
try:
archivo = open("datos.txt", "r")
contenido = archivo.read()
finally:
archivo.close()
# Con context manager (recomendado)
with open("datos.txt", "r") as archivo:
contenido = archivo.read()
# El archivo se cierra automáticamente al salir del bloque with
3. Validación de entrada con excepciones
def obtener_edad():
while True:
try:
edad = int(input("Introduce tu edad: "))
if not 0 <= edad <= 120:
raise ValueError("La edad debe estar entre 0 y 120")
return edad
except ValueError as e:
print(f"Error: {e}")
print("Por favor, inténtalo de nuevo.")
Mejores prácticas en el manejo de excepciones
- Sé específico: Captura excepciones específicas en lugar de usar un bloque
except
genérico. - Mantén los bloques try pequeños: Incluye solo el código que podría generar la excepción que estás tratando de capturar.
- No silencies excepciones: Evita bloques
except
vacíos o que simplemente pasan sin hacer nada. - Usa finally para la limpieza: Utiliza el bloque
finally
para código de limpieza que debe ejecutarse siempre. - Prefiere context managers: Usa
with
para recursos que necesitan ser liberados. - Documenta las excepciones: En las docstrings de tus funciones, documenta las excepciones que pueden ser lanzadas.
- Lanza excepciones apropiadas: Cuando crees tus propias excepciones, elige el tipo adecuado y proporciona mensajes claros.
- No uses excepciones para flujo de control normal: Las excepciones deben ser para situaciones excepcionales, no para controlar el flujo normal del programa.
Ejemplos prácticos
Ejemplo 1: Calculadora robusta
def calculadora():
operaciones = {
"+": lambda x, y: x + y,
"-": lambda x, y: x - y,
"*": lambda x, y: x * y,
"/": lambda x, y: x / y
}
try:
num1 = float(input("Introduce el primer número: "))
num2 = float(input("Introduce el segundo número: "))
op = input("Introduce la operación (+, -, *, /): ")
if op not in operaciones:
raise ValueError("Operación no válida")
resultado = operaciones[op](num1, num2)
print(f"Resultado: {resultado}")
except ValueError as e:
if str(e) == "Operación no válida":
print(f"Error: {e}")
else:
print("Error: Debes introducir números válidos.")
except ZeroDivisionError:
print("Error: No puedes dividir entre cero.")
except Exception as e:
print(f"Error inesperado: {e}")
Ejemplo 2: Gestor de archivos seguro
def leer_archivo_seguro(ruta_archivo):
try:
with open(ruta_archivo, "r", encoding="utf-8") as archivo:
return archivo.read()
except FileNotFoundError:
print(f"El archivo {ruta_archivo} no existe.")
return None
except PermissionError:
print(f"No tienes permiso para leer el archivo {ruta_archivo}.")
return None
except UnicodeDecodeError:
print(f"No se puede decodificar el archivo {ruta_archivo} con UTF-8.")
try:
with open(ruta_archivo, "r", encoding="latin-1") as archivo:
return archivo.read()
except Exception:
return None
except Exception as e:
print(f"Error inesperado al leer {ruta_archivo}: {e}")
return None
# Uso
contenido = leer_archivo_seguro("datos.txt")
if contenido:
print("Contenido del archivo:")
print(contenido)
else:
print("No se pudo leer el archivo.")
Ejemplo 3: API de validación con excepciones personalizadas
class ValidacionError(Exception):
"""Clase base para errores de validación."""
pass
class LongitudInvalidaError(ValidacionError):
"""Error de longitud inválida."""
pass
class FormatoInvalidoError(ValidacionError):
"""Error de formato inválido."""
pass
def validar_nombre_usuario(username):
"""
Valida un nombre de usuario.
Args:
username (str): El nombre de usuario a validar.
Returns:
bool: True si el nombre de usuario es válido.
Raises:
TypeError: Si el nombre de usuario no es una cadena.
LongitudInvalidaError: Si el nombre de usuario no tiene entre 3 y 20 caracteres.
FormatoInvalidoError: Si el nombre de usuario contiene caracteres no permitidos.
"""
if not isinstance(username, str):
raise TypeError("El nombre de usuario debe ser una cadena")
if not 3 <= len(username) <= 20:
raise LongitudInvalidaError("El nombre de usuario debe tener entre 3 y 20 caracteres")
import re
if not re.match(r'^[a-zA-Z0-9_]+$', username):
raise FormatoInvalidoError("El nombre de usuario solo puede contener letras, números y guiones bajos")
return True
# Uso
def registrar_usuario():
try:
username = input("Introduce un nombre de usuario: ")
validar_nombre_usuario(username)
print(f"Nombre de usuario '{username}' válido. Usuario registrado.")
except ValidacionError as e:
print(f"Error de validación: {e}")
except Exception as e:
print(f"Error inesperado: {e}")
registrar_usuario()
Ejemplo 4: Manejo de timeout en operaciones de red
import requests
import time
def obtener_datos_con_reintentos(url, max_intentos=3, timeout=5):
"""
Obtiene datos de una URL con reintentos en caso de fallos.
Args:
url (str): La URL para obtener los datos.
max_intentos (int): Número máximo de intentos.
timeout (int): Tiempo máximo de espera por intento en segundos.
Returns:
dict: Los datos obtenidos o None si fallan todos los intentos.
"""
for intento in range(1, max_intentos + 1):
try:
print(f"Intento {intento} de {max_intentos}...")
respuesta = requests.get(url, timeout=timeout)
respuesta.raise_for_status() # Lanza una excepción para códigos de error HTTP
return respuesta.json()
except requests.exceptions.Timeout:
print(f"Timeout en el intento {intento}.")
except requests.exceptions.RequestException as e:
print(f"Error en la solicitud: {e}")
if intento < max_intentos:
# Espera exponencial entre reintentos
tiempo_espera = 2 ** (intento - 1)
print(f"Esperando {tiempo_espera} segundos antes del siguiente intento...")
time.sleep(tiempo_espera)
print("Se han agotado todos los intentos.")
return None
# Uso
datos = obtener_datos_con_reintentos("https://jsonplaceholder.typicode.com/posts/1")
if datos:
print("Datos obtenidos:", datos)
else:
print("No se pudieron obtener los datos.")
Ejercicios prácticos
- Crea una función que lea un archivo CSV y maneje posibles errores como archivo no encontrado, formato incorrecto, etc.
- Implementa un sistema de validación de formularios que use excepciones personalizadas para diferentes tipos de errores de validación.
- Escribe una función que convierta diferentes unidades de medida (longitud, peso, temperatura) y maneje adecuadamente entradas inválidas.
- Crea un programa que realice operaciones en una base de datos SQLite y maneje las posibles excepciones que puedan ocurrir.
- Implementa un cliente HTTP simple que maneje diferentes tipos de errores de red y códigos de estado HTTP.
En la próxima lección, exploraremos la programación orientada a objetos en Python, que nos permitirá organizar nuestro código en clases y objetos para modelar conceptos del mundo real.