Saltar a contenido

2.3.-Captura de excepciones

2.3. Control de excepciones

Idea principal

Una excepción es un evento que altera el flujo normal de ejecución, por ejemplo, una entrada que no se puede convertir a número o un archivo que no existe. En Python se puede capturar, tratar, propagar o generar de forma explícita, pero manejar una excepción no sustituye a corregir los errores del código.

Un programa real recibe datos de personas usuarias, lee ficheros, consulta servicios y realiza operaciones que dependen de condiciones externas. Aunque el código esté bien escrito, alguna de esas condiciones puede no cumplirse. El control de excepciones permite responder de forma previsiblemente segura: informar, solicitar otro dato, usar una alternativa o dejar que el error llegue a quien pueda resolverlo.

La palabra error se usa a menudo con un sentido general, mientras que una excepción es el mecanismo concreto que Python utiliza para representar muchas situaciones anómalas durante la ejecución. Una excepción puede revelar un bug del programa, pero también puede representar una situación prevista, como que una persona escriba texto donde se esperaba un número.

Este tema se relaciona con el RA 3 del módulo de Programación: «Escribe y depura código, analizando y utilizando las estructuras de control del lenguaje». La normativa del módulo establece los siguientes criterios de evaluación relacionados:

Código Descripción literal de la normativa
RA 3 Escribe y depura código, analizando y utilizando las estructuras de control del lenguaje.
CE 3.d Se ha escrito código utilizando control de excepciones.
CE 3.h Se han creado excepciones.
CE 3.i Se han utilizado aserciones para la detección y corrección de errores durante la fase de desarrollo.

Se trabajan los criterios que tienen que ver con la captura de excepciones, la creación de excepciones y el uso de aserciones para detectar errores durante el desarrollo.

Qué deberías saber al terminar

Al acabar este tema deberías poder:

  • distinguir errores de sintaxis, de lógica y excepciones de ejecución;
  • escribir bloques try-except que capturen excepciones concretas;
  • separar el camino correcto con else y liberar recursos con finally o with;
  • lanzar excepciones propias con raise y dejar que se propaguen cuando corresponda;
  • comprobar con pruebas que una función produce la excepción esperada;
  • interpretar un traceback y evitar que un manejo demasiado amplio oculte bugs.

Mapa del tema

En este documento seguiremos esta progresión:

  1. errores, excepciones y tracebacks;
  2. estructura try-except y captura de excepciones concretas;
  3. cláusulas else, finally y gestión de recursos;
  4. lanzamiento, propagación, excepciones personalizadas y aserciones;
  5. validación de entradas, pruebas y criterios de diseño.

1. Errores, excepciones y tracebacks

No todos los problemas de un programa se detectan de la misma forma. Antes de decidir cómo manejar una situación, hay que identificar qué clase de problema se ha producido.

Excepción

Una excepción es un objeto que representa una condición anómala durante la ejecución. Si no se captura en el punto adecuado, Python interrumpe el flujo normal y la propaga hacia el código que ha llamado a la operación. Si nadie la maneja, el programa termina y muestra un traceback.

1.1. Tipos de problemas

Problema Cuándo aparece Ejemplo
Error de sintaxis Python no puede interpretar el código. Falta : al terminar una cabecera if.
Error de lógica El programa se ejecuta, pero calcula o decide algo incorrecto. Calcular Celsius con una fórmula equivocada.
Excepción de ejecución Una operación no puede completarse con el estado o los datos actuales. float("rojo") produce ValueError.

Un error lógico no tiene por qué producir una excepción. Por ejemplo, una función que suma 2 + 2 y devuelve 5 puede ejecutarse sin que Python detecte nada anómalo. En cambio, una excepción puede ser una situación esperable que el programa sabe gestionar. Por eso conviene no afirmar que «los errores se corrigen y las excepciones se ignoran»: las excepciones también pueden revelar errores de diseño o de implementación.

Conversión de una entrada

En el intérprete interactivo, una excepción no manejada devuelve el control al indicador >>>. En un script, detiene el script si no existe un manejador. La operación siguiente no se ejecuta.

entrada = input("Introduce la temperatura en Fahrenheit: ")
fahrenheit = float(entrada)
celsius = (fahrenheit - 32.0) * 5.0 / 9.0
print(celsius)

Una entrada como 72 permite continuar; una entrada como fred provoca ValueError al ejecutar float(entrada). El problema no está en que float() funcione mal, sino en que el dato recibido no cumple el formato que la operación necesita.

1.2. Excepciones habituales de Python

Python dispone de una jerarquía de excepciones predefinidas. Estas son algunas de las más frecuentes en los primeros programas:

Excepción Situación habitual
ValueError El tipo es adecuado, pero el valor no tiene el formato esperado.
TypeError Una operación recibe un tipo que no puede utilizar.
ZeroDivisionError Se intenta dividir entre cero.
FileNotFoundError No se encuentra el fichero solicitado.
PermissionError El sistema no permite la operación sobre un recurso.
IndexError Se accede a una posición inexistente de una secuencia.
KeyError Se solicita una clave que no existe en un diccionario.
NameError Se utiliza un nombre que no está definido.
KeyboardInterrupt La persona usuaria interrumpe normalmente con Ctrl+C.

KeyboardInterrupt y SystemExit no heredan de Exception, sino directamente de BaseException. No se deben capturar indiscriminadamente todas las subclases de BaseException, porque se podría impedir que el programa se cierre o se interrumpa de forma normal.

1.3. Cómo leer un traceback

Cuando una excepción no se maneja, Python muestra un informe similar a este:

Traceback (most recent call last):
  File "fahren.py", line 2, in <module>
    fahrenheit = float(entrada)
ValueError: could not convert string to float: 'fred'

Para investigarlo:

  1. Lee la última línea: indica el tipo de excepción y su mensaje.
  2. Localiza el archivo y la línea donde Python detectó la excepción.
  3. Revisa los valores y las llamadas anteriores que llevaron hasta ese punto.
  4. Decide si hay que corregir un bug, validar una entrada o manejar una situación prevista.

La línea señalada es el lugar en el que Python detecta el problema, pero no siempre es el lugar en el que se originó la decisión incorrecta. Esta distinción será útil al depurar programas más grandes.

2. try-except: capturar excepciones

La estructura try-except permite ejecutar una operación que puede fallar y definir qué hacer si se produce una excepción concreta:

try:
    instrucciones_que_pueden_fallar()
except TipoDeExcepcion:
    instrucciones_para_tratarla()

Python ejecuta primero el bloque try. Si termina sin excepciones, omite el bloque except y continúa después de la estructura. Si se produce una excepción compatible, abandona el resto del bloque try y ejecuta el primer except que corresponda.

flowchart TD
    A["Comenzar el bloque try"] --> B{"¿Se produce una excepción?"}
    B -->|No| C["Ejecutar else, si existe"]
    B -->|Sí| D{"¿Hay un except compatible?"}
    D -->|Sí| E["Ejecutar el primer except compatible"]
    D -->|No| F["Propagar la excepción"]
    C --> G["Ejecutar finally, si existe"]
    E --> G
    F --> H["Mostrar traceback o llegar al llamador"]
    G --> I["Continuar después de la estructura"]

2.1. Conversión segura de una temperatura

El conversor de Fahrenheit puede informar de una entrada incorrecta sin mostrar un traceback a la persona usuaria:

entrada = input("Introduce la temperatura en Fahrenheit: ")

try:
    fahrenheit = float(entrada)
    celsius = (fahrenheit - 32.0) * 5.0 / 9.0
except ValueError:
    print("Por favor, introduce un número válido.")
else:
    print(f"{fahrenheit} °F equivalen a {celsius:.2f} °C.")

En este caso, ValueError cubre el fallo de conversión. La fórmula queda dentro del mismo try porque también depende de haber obtenido un valor numérico; el mensaje de éxito se coloca en else para que solo se muestre cuando el cálculo ha terminado correctamente.

Capturar no significa ocultar

Un bloque except debe resolver, comunicar o propagar una situación concreta. Un except vacío que imprime simplemente «Error» puede ocultar un bug, dificultar el diagnóstico y dejar el programa en un estado incoherente.

2.2. El tamaño del bloque try

Conviene incluir en try solo las instrucciones que pueden producir la excepción que se quiere gestionar:

entrada = input("Introduce un número: ")

try:
    numero = int(entrada)
except ValueError:
    print("La entrada no es un entero.")
else:
    resultado = numero * 2
    print(f"Resultado: {resultado}")

Si se coloca todo el programa dentro de un try demasiado amplio, un error de lógica o un fallo de una función distinta podría terminar mostrando el mensaje destinado a la conversión. Un bloque pequeño hace que el origen y la respuesta sean más claros.

3. Capturas específicas y cláusulas de control

Una aplicación suele tener que distinguir varias causas posibles. Se pueden escribir varios except o agrupar excepciones que requieren exactamente el mismo tratamiento.

Construcción Para qué sirve Ejemplo
except ValueError: Trata una excepción concreta. Entrada con formato no numérico.
except (A, B): Aplica el mismo tratamiento a dos tipos. ValueError y TypeError.
except Tipo as error: Conserva el objeto de excepción. Mostrar o registrar su mensaje.
except Exception: Captura la mayoría de excepciones de aplicación. Límite controlado en una frontera de la aplicación.

3.1. Varios manejadores y el orden de evaluación

try:
    numero = int(input("Introduce un número entero: "))
    resultado = 100 / numero
except ValueError as error:
    print(f"La entrada no es válida: {error}")
except ZeroDivisionError:
    print("No se puede dividir entre cero.")
else:
    print(f"El resultado es {resultado}.")

Python comprueba los manejadores de arriba abajo y ejecuta solo el primero que sea compatible. Las excepciones más específicas deben aparecer antes que las generales:

try:
    procesar_dato()
except ValueError:
    print("El dato tiene un valor incorrecto.")
except Exception:
    print("Se ha producido un fallo de aplicación.")

Si except Exception apareciera primero, también capturaría ValueError y el manejador específico quedaría inalcanzable. Un except Exception puede tener sentido en el límite de una aplicación si registra el error y lo comunica de forma segura, pero no debe convertirse en una forma de silenciar fallos.

Para dar el mismo tratamiento a varias excepciones se utiliza una tupla:

try:
    dato = convertir_dato()
except (ValueError, TypeError) as error:
    print(f"No se puede convertir el dato: {error}")

3.2. La cláusula else

else se ejecuta únicamente cuando el bloque try termina sin excepciones. Es útil para separar la operación arriesgada del código que depende de que haya tenido éxito:

try:
    numero = int(input("Número: "))
except ValueError:
    print("Entrada inválida.")
else:
    print(f"Procesando {numero}...")
    print(f"Resultado: {numero * 2}")

No es obligatorio utilizar else, pero suele mejorar la lectura cuando el camino correcto contiene varias instrucciones. Si una instrucción del bloque else falla, la excepción no se considera producida dentro del try; se propagará a un manejador exterior salvo que exista otro control.

3.3. La cláusula finally

finally se ejecuta siempre al abandonar la estructura, tanto si hubo excepción como si no. Es apropiado para acciones de limpieza que no deben olvidarse:

archivo = None

try:
    archivo = open("datos.txt", encoding="utf-8")
    contenido = archivo.read()
    print(contenido)
except FileNotFoundError:
    print("El archivo no existe.")
finally:
    if archivo is not None:
        archivo.close()

El bloque finally también se ejecuta cuando se utiliza return, break o continue dentro de la estructura. No conviene poner otro return dentro de finally, porque puede ocultar un valor o una excepción que se estaba propagando.

Para ficheros, la forma preferida es utilizar un gestor de contexto con with, que garantiza el cierre cuando termina el bloque:

try:
    with open("datos.txt", encoding="utf-8") as archivo:
        contenido = archivo.read()
except FileNotFoundError:
    print("El archivo no existe.")
else:
    print(f"Se han leído {len(contenido)} caracteres.")

El gestor de contexto equivale conceptualmente a preparar una limpieza con try-finally, pero expresa mejor la intención y reduce la posibilidad de dejar recursos abiertos. El mismo criterio se aplica a conexiones, bloqueos y otros recursos que tengan una operación de liberación.

4. Lanzar, propagar y crear excepciones

Hasta ahora hemos tratado excepciones generadas por Python o por una biblioteca. Una función también puede detectar que sus argumentos no cumplen el contrato y comunicarlo mediante raise.

4.1. La sentencia raise

raise genera una excepción de forma explícita. Se puede proporcionar una instancia con mensaje o una clase, que Python instancia sin argumentos. En código de aplicación, las clases de excepción suelen heredar de Exception, que a su vez hereda de BaseException:

raise NameError("HiThere")

raise ValueError("La temperatura no es válida.")

# Forma abreviada equivalente:
raise ValueError

Una función que convierte Fahrenheit puede validar además el límite físico del cero absoluto:

def fahrenheit_a_celsius(fahrenheit: float) -> float:
    """Convierte una temperatura y rechaza valores físicamente imposibles."""
    if fahrenheit < -459.67:
        raise ValueError(
            f"Temperatura inferior al cero absoluto: {fahrenheit} °F"
        )

    return (fahrenheit - 32.0) * 5.0 / 9.0

La persona que llama decide si puede recuperar la situación:

try:
    celsius = fahrenheit_a_celsius(-500)
except ValueError as error:
    print(f"No se puede realizar la conversión: {error}")

La excepción describe una condición inválida sin obligar a la función a decidir cómo debe mostrarse el mensaje o qué alternativa debe utilizar la aplicación.

4.2. Volver a lanzar y encadenar excepciones

Una capa puede registrar una excepción y volver a lanzarla para que una capa superior tome la decisión final:

def leer_configuracion(ruta: str) -> str:
    try:
        with open(ruta, encoding="utf-8") as archivo:
            return archivo.read()
    except FileNotFoundError:
        print(f"No se encuentra la configuración: {ruta}")
        raise

La sentencia raise sin argumento solo es válida dentro del manejador activo y conserva la excepción y su traceback. Si se captura una excepción para convertirla en otra más útil, raise ... from ... conserva la causa original:

import json


def cargar_json(ruta: str) -> dict:
    try:
        with open(ruta, encoding="utf-8") as archivo:
            return json.load(archivo)
    except json.JSONDecodeError as error:
        raise ValueError(f"El JSON de {ruta} no es válido") from error

Así, quien usa cargar_json recibe un error de dominio claro y quien depura puede consultar la causa original.

4.3. Excepciones personalizadas

Cuando un proyecto tiene una condición de error propia, se puede crear una clase que herede de una excepción apropiada. Si el problema es un valor inválido, heredar de ValueError permite que también se trate como un error de valor:

class TemperaturaInvalidaError(ValueError):
    """Indica que una temperatura está fuera del intervalo permitido."""


def validar_celsius(celsius: float) -> None:
    if celsius < -273.15:
        raise TemperaturaInvalidaError(
            "La temperatura es inferior al cero absoluto."
        )

Una clase de excepción personalizada debe aportar un nombre que explique el problema. No es necesario añadir métodos ni atributos mientras no exista una necesidad real; el mensaje y la jerarquía ya permiten un tratamiento diferenciado.

4.4. Aserciones para desarrollo

La sentencia assert comprueba una condición y lanza AssertionError si es falsa:

def calcular_media(valores: list[float]) -> float:
    assert valores, "La lista no puede estar vacía durante el desarrollo."
    return sum(valores) / len(valores)

Las aserciones sirven para detectar invariantes y errores de programación durante el desarrollo. No deben sustituir a la validación de datos externos o de entradas de usuario, porque Python puede desactivarlas al ejecutar con la opción -O:

python -O programa.py

Para validar datos que no controla el programa, es preferible utilizar una condición explícita y raise ValueError u otra excepción adecuada.

5. Aplicación: validar entradas y probar el comportamiento

El patrón más habitual al comenzar es repetir la petición hasta que la entrada tenga un formato y un valor aceptables. La solución debe distinguir la conversión del dato, la validación de sus reglas y el tratamiento de una interrupción voluntaria.

Para diseñar este tipo de solución:

  1. Define el contrato: decide qué datos son válidos y qué excepción comunica cada incumplimiento.
  2. Aísla la operación arriesgada: coloca en try la conversión o la llamada que puede fallar.
  3. Captura lo que sabes tratar: muestra un mensaje útil o solicita otro valor solo para excepciones esperadas.
  4. Deja continuar lo inesperado: registra y propaga los fallos que el código no sabe resolver.
  5. Prueba ambos caminos: comprueba entradas válidas, límites, datos erróneos e interrupciones relevantes.

El siguiente programa reúne estos pasos para el conversor de temperaturas:

def fahrenheit_a_celsius(fahrenheit: float) -> float:
    """Convierte Fahrenheit a Celsius tras validar el cero absoluto."""
    if fahrenheit < -459.67:
        raise ValueError(
            f"Temperatura inferior al cero absoluto: {fahrenheit} °F"
        )
    return (fahrenheit - 32.0) * 5.0 / 9.0


def pedir_temperatura() -> float:
    """Solicita una temperatura hasta recibir un valor válido."""
    valida = False
    celsius = 0.0

    while not valida:
        texto = input("Introduce la temperatura en Fahrenheit: ")
        try:
            fahrenheit = float(texto)
            celsius = fahrenheit_a_celsius(fahrenheit)
        except ValueError as error:
            print(f"Entrada no válida: {error}")
        else:
            valida = True

    return celsius


if __name__ == "__main__":
    try:
        celsius = pedir_temperatura()
    except KeyboardInterrupt:
        print("\nPrograma interrumpido por la persona usuaria.")
    else:
        print(f"Temperatura en Celsius: {celsius:.2f} °C")

El try del bucle captura tanto un texto no numérico como una temperatura inferior al límite, porque en ambos casos se lanza ValueError y la respuesta adecuada es solicitar otro dato. La interrupción con Ctrl+C se trata fuera del bucle para permitir una salida limpia, pero no se captura con un except genérico.

5.1. Diseñar una prueba de excepción con pytest

Una prueba no debe comprobar únicamente los resultados correctos. También debe verificar que una función rechaza los casos que forman parte de su contrato. Supongamos este módulo:

# calculadora.py
def dividir(dividendo: float, divisor: float) -> float:
    """Devuelve el cociente; Python lanza ZeroDivisionError si procede."""
    return dividendo / divisor

La prueba se guarda en un archivo separado, por ejemplo test_calculadora.py:

import pytest

from calculadora import dividir


def test_dividir_por_cero_deberia_lanzar_excepcion():
    with pytest.raises(ZeroDivisionError):
        dividir(10, 0)


def test_dividir_devuelve_el_cociente():
    assert dividir(10, 2) == 5

pytest.raises(ZeroDivisionError) hace que la prueba pase solo si la llamada del bloque genera esa excepción. Si no se genera ninguna o se genera otra distinta, la prueba falla. También se puede comprobar parte del mensaje con match cuando el texto de la excepción forma parte del contrato:

# test_conversor.py
import pytest

from conversor import fahrenheit_a_celsius


def test_temperatura_inferior_al_cero_absoluto():
    with pytest.raises(ValueError, match="cero absoluto"):
        fahrenheit_a_celsius(-500)

Las pruebas deberían cubrir el camino normal, las fronteras del dominio y los fallos previsibles. Así se comprueba no solo que el programa «no se cae», sino que responde de la forma acordada.

6. Buenas prácticas

Para trabajar con excepciones con criterio profesional, conviene aplicar estas pautas:

  • Captura excepciones específicas: relaciona cada manejador con una situación que sepas resolver.
  • Mantén los bloques try pequeños: así no confundes un fallo de la operación con un fallo del propio tratamiento.
  • Usa mensajes útiles: incluye la operación, el dato relevante y el formato o rango esperado sin exponer información sensible.
  • No ocultes errores: evita except: pass; registra, corrige, transforma o vuelve a lanzar el error.
  • Separa éxito y fallo: utiliza else para el camino correcto y finally o with para la limpieza.
  • Usa excepciones para situaciones excepcionales: para decisiones normales, una condición suele ser más clara que provocar una excepción deliberadamente.
  • Valida entradas externas: no uses assert como sustituto de la validación de datos de una persona usuaria o de un servicio.
  • Limita los reintentos: una operación de red o de acceso a recursos no debe repetirse indefinidamente.
  • Prueba el contrato completo: incluye resultados válidos, límites, excepciones esperadas y propagación de fallos inesperados.

Python suele describirse con el patrón EAFP (Easier to Ask for Forgiveness than Permission): se intenta la operación y se captura una excepción conocida si falla. En otros casos, una comprobación previa (LBYL, Look Before You Leap) es más clara. La elección depende de la operación; no se trata de utilizar excepciones como sustituto universal de los if.

7. Errores frecuentes

Error frecuente Por qué ocurre Cómo evitarlo
Usar except: sin tipo. Se capturan también fallos que no se habían previsto y se pierde información. Captura excepciones concretas; usa Exception solo en una frontera controlada y con registro o propagación.
Colocar except Exception antes de uno específico. El manejador general también coincide con la excepción específica. Ordena los manejadores de lo específico a lo general.
Incluir todo el programa dentro de try. Un error no relacionado parece un fallo de entrada. Protege únicamente la operación que puede producir la excepción.
Usar except: pass. El programa continúa sin resultado válido y el bug queda oculto. Comunica, registra, corrige o vuelve a lanzar la excepción.
Cerrar un recurso solo al final del camino correcto. Una excepción puede saltar la instrucción close(). Usa with o garantiza la limpieza mediante finally.
Acceder a archivo en finally sin inicializarlo. La apertura pudo fallar antes de asignar la variable. Inicializa con None y comprueba el valor, o utiliza with.
Ejecutar raise fuera de un manejador. El raise sin expresión solo puede relanzar una excepción activa. Usa raise Tipo(...) para generar una nueva o relanza dentro de except.
Tratar una aserción como validación de entrada. assert puede desactivarse con python -O. Valida entradas con condiciones y excepciones explícitas.
Confundir un error lógico con una excepción. Se espera que Python detecte resultados incorrectos por sí solo. Diseña pruebas con resultados esperados y casos límite.
Probar solo el camino correcto. El programa funciona con datos ideales, pero falla ante entradas reales. Prueba éxito, formato inválido, límites, recursos ausentes e interrupciones.

8. Resumen

En este tema has aprendido que:

  • los errores de sintaxis, los errores lógicos y las excepciones de ejecución requieren diagnósticos diferentes;
  • try-except permite capturar una excepción, pero se deben indicar tipos concretos y manejar solo lo que se sabe resolver;
  • else separa el camino correcto, finally garantiza la limpieza y with facilita la gestión de recursos;
  • raise permite expresar contratos y validaciones, mientras que raise sin argumento vuelve a lanzar la excepción activa;
  • las excepciones personalizadas hacen más expresivo el dominio del programa y assert ayuda a detectar invariantes durante el desarrollo;
  • pytest.raises verifica que una función produce la excepción esperada, y las pruebas deben cubrir también los casos límite.

Idea clave

Un programa robusto no intenta esconder todos los fallos: distingue las situaciones recuperables, las trata con precisión, libera sus recursos y deja que los errores inesperados conserven la información necesaria para corregirlos.

9. Para seguir practicando

  • Completa la práctica de control de excepciones, que incluye validación de entradas y lanzamiento de excepciones.
  • Resuelve la actividad de la división entre cero y muestra un mensaje que explique la excepción capturada.
  • Reescribe el conversor de temperaturas para que repita la petición hasta recibir un número válido.
  • Añade pruebas con pytest.raises para una función que rechace valores fuera de rango.
  • Continúa con depuración de programas para investigar tracebacks, variables y flujo de ejecución.

Bibliografía y fuentes

Presentación

Puedes repasar el control de excepciones con la presentación de la unidad 2.3.