HDP115

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.

CE

Cristian Escalante

Última actualización: 21 de mayo de 2025

python
programación
control de errores

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

  1. Sé específico: Captura excepciones específicas en lugar de usar un bloque except genérico.
  2. 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.
  3. No silencies excepciones: Evita bloques except vacíos o que simplemente pasan sin hacer nada.
  4. Usa finally para la limpieza: Utiliza el bloque finally para código de limpieza que debe ejecutarse siempre.
  5. Prefiere context managers: Usa with para recursos que necesitan ser liberados.
  6. Documenta las excepciones: En las docstrings de tus funciones, documenta las excepciones que pueden ser lanzadas.
  7. Lanza excepciones apropiadas: Cuando crees tus propias excepciones, elige el tipo adecuado y proporciona mensajes claros.
  8. 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

  1. Crea una función que lea un archivo CSV y maneje posibles errores como archivo no encontrado, formato incorrecto, etc.
  2. Implementa un sistema de validación de formularios que use excepciones personalizadas para diferentes tipos de errores de validación.
  3. Escribe una función que convierta diferentes unidades de medida (longitud, peso, temperatura) y maneje adecuadamente entradas inválidas.
  4. Crea un programa que realice operaciones en una base de datos SQLite y maneje las posibles excepciones que puedan ocurrir.
  5. 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.

Funciones en Python
Aprende a crear y utilizar funciones en Python para organiza...
Programación Orientada a Objetos
Aprende los fundamentos de la programación orientada a objet...
Referencias
Python.org. Python Exceptions. https://docs.python.org/3/tutorial/errors.html
Python.org. Built-in Exceptions. https://docs.python.org/3/library/exceptions.html
Real Python. Python Exception Handling Techniques. https://realpython.com/python-exceptions/

Conceptos Básicos de HTML

Aprende los conceptos básicos de HTML

Conceptos Básicos de CSS

Aprende los conceptos básicos de CSS

Conceptos Básicos de JavaScript

Aprende los conceptos básicos de JavaScript

Conceptos Básicos SQL

Aprende los conceptos básicos de SQL

Conceptos Básicos de GIT

Aprende los conceptos básicos de GIT

Conceptos Básicos de UML

Aprende los conceptos básicos de UML

Refuerzo Academico de Herramientas de Productividad 2025