Apuntes sobre Python

La indentación no es estilo: es sintaxis.

Excepciones

A lo largo del manual ya te has cruzado con varias excepciones: TypeError al intentar sumar una cadena con un entero, ValueError al convertir "hola" a número, IndexError al pedir un elemento de una lista que no existe, FileNotFoundError al abrir un archivo que no está. Esta lección trata de qué son exactamente y cómo manejarlas sin que el programa se caiga.

Qué es una excepción

Una excepción es un evento que interrumpe el flujo normal del programa cuando algo va mal: una operación imposible, datos inválidos, un recurso que no está disponible. Si no se gestiona, Python detiene el programa y muestra un traceback: un informe de dónde ocurrió el problema:

>>> edad = int("veinte")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: invalid literal for int() with base 10: 'veinte'

El traceback se lee de arriba abajo: la línea donde estabas, y al final el tipo de excepción (ValueError) con un mensaje explicando qué pasó. Aprender a leer tracebacks es de las habilidades que más rinde al principio: casi siempre te están diciendo exactamente lo que está mal.

Capturar excepciones con try/except

Para que el programa pueda reaccionar a un error en lugar de pararse, se envuelve el código sospechoso en un bloque try y se indica qué hacer si falla en un bloque except:

>>> entrada = "veinte"
>>> try:
...     edad = int(entrada)
...     print("la edad es", edad)
... except ValueError:
...     print("eso no es un número válido")
eso no es un número válido

Si el código dentro del try se ejecuta sin problemas, el bloque except se ignora. Si lanza una excepción del tipo indicado, Python salta inmediatamente al except correspondiente y sigue desde ahí. El programa continúa, no se cae.

Varios except para excepciones distintas

Un mismo try puede ir seguido de varios except, cada uno para un tipo de error distinto:

try:
    numero = int(input("escribe un número: "))
    resultado = 100 / numero
    print("100 dividido entre", numero, "es", resultado)
except ValueError:
    print("eso no era un número")
except ZeroDivisionError:
    print("no se puede dividir entre cero")

Python prueba los except en orden y ejecuta el primero cuyo tipo coincida con la excepción lanzada. Si no coincide ninguno, la excepción se propaga hacia fuera (y, si nadie la captura, acaba en un traceback).

Si varias excepciones se manejan igual, puedes agruparlas en una tupla:

try:
    resultado = 100 / int(entrada)
except (ValueError, ZeroDivisionError):
    print("entrada no válida")

Acceder al objeto de la excepción

A veces necesitas el mensaje exacto del error, por ejemplo para registrarlo en un log. Usa as para capturar el objeto de la excepción en una variable:

>>> try:
...     int("veinte")
... except ValueError as e:
...     print("error capturado:", e)
error capturado: invalid literal for int() with base 10: 'veinte'

La variable e es el objeto excepción completo: imprimirlo muestra el mensaje, pero también tiene atributos y se puede inspeccionar con type(e).

except Exception: el cajón de sastre

Si quieres capturar cualquier error que pueda ocurrir, usa except Exception. Casi todas las excepciones que puedes encontrarte heredan de la clase Exception, así que ese except las cubre todas:

try:
    operacion_arriesgada()
except Exception as e:
    print("algo ha ido mal:", e)

Nota: captura excepciones específicas siempre que puedas. Un except Exception genérico esconde bugs reales: el día que tengas un error de tipo distinto, lo capturarás silenciosamente y no te enterarás. Úsalo solo en el "borde" del programa (un bucle principal que no debe morir, por ejemplo), nunca como sustituto de pensar qué errores concretos esperas.

Hay también un except sin tipo (except: a secas) que captura literalmente todo, incluidas señales como Ctrl+C. No lo uses nunca.

Las cláusulas else y finally

Además de try y except, hay dos cláusulas más:

  • else: se ejecuta solo si no hubo excepción. Sirve para separar el "happy path" del manejo de error.
  • finally: se ejecuta siempre, hubo error o no. Sirve para tareas de limpieza que tienen que pasar pase lo que pase.
try:
    f = open("datos.txt")
    contenido = f.read()
except FileNotFoundError:
    print("el archivo no existe")
else:
    print("archivo leído correctamente")
    print(contenido[:50])
finally:
    print("este mensaje se imprime siempre")

Nota: la cláusula finally se usaba antes para cerrar archivos pase lo que pase. Hoy en día eso lo resuelve la construcción with de manera mucho más limpia. Sigue siendo útil en otros contextos: cerrar conexiones de red, liberar bloqueos, restaurar estado.

Lanzar excepciones con raise

Hasta ahora hemos hablado de capturar excepciones que Python lanza por su cuenta. Pero tú también puedes lanzar tus propias excepciones con raise, útil cuando detectas algo inválido y quieres detener el flujo en ese punto:

def aplicar_descuento(precio, porcentaje):
    if porcentaje < 0 or porcentaje > 100:
        raise ValueError(f"porcentaje fuera de rango: {porcentaje}")
    return precio * (1 - porcentaje / 100)

>>> aplicar_descuento(50, 120)
ValueError: porcentaje fuera de rango: 120

Lanzar una excepción es preferible a "devolver None si algo va mal": deja claro al que llame que la condición de error es realmente excepcional, e impide que el resto del código siga ejecutándose con datos inválidos.

Las excepciones más usadas para esto son ValueError (valor del tipo correcto pero contenido inválido), TypeError (tipo incorrecto) y RuntimeError (cajón genérico).

Relanzar después de actuar

Una variante útil de raise es la que aparece dentro de un except: capturas el error, haces algo con él (registrarlo, avisar al usuario), y lo relanzas para que siga propagándose:

try:
    procesar_archivo("datos.csv")
except Exception as e:
    print(f"error procesando datos.csv: {e}")
    raise   # vuelve a lanzar la misma excepción

El raise a secas (sin argumento), dentro de un except, relanza la excepción actual. Es el patrón cuando quieres dejar constancia del error pero no quieres tragártelo.

Excepciones más comunes

No tienes que memorizarlas, pero saber qué significan ayuda a interpretar tracebacks de un vistazo:

ExcepciónCuándo aparece
SyntaxErrorEl código no es Python válido. No se llega ni a ejecutar.
IndentationErrorVariante de SyntaxError por indentación incorrecta.
NameErrorUsas un nombre que no está definido (variable mal escrita, falta un import).
TypeErrorOperación con tipos incompatibles ("hola" + 5) o argumentos incorrectos.
ValueErrorEl tipo está bien pero el valor no es válido (int("hola")).
IndexErrorÍndice fuera de rango en una lista o tupla.
KeyErrorClave que no existe en un diccionario.
AttributeErrorAtributo o método inexistente (None.upper()).
ZeroDivisionErrorDivisión entre cero.
FileNotFoundErrorIntentas abrir un archivo que no existe.
PermissionErrorNo tienes permisos para leer o escribir un archivo.
KeyboardInterruptEl usuario ha pulsado Ctrl+C.
ImportErrorNo se puede importar un módulo (no instalado, mal escrito).

EAFP: pide perdón en vez de permiso

Existen dos formas de protegerse contra errores antes de que ocurran. La primera es comprobar antes de actuar (look before you leap); la segunda es intentar y capturar (easier to ask forgiveness than permission, abreviado EAFP). En Python, la segunda es la idiomática:

# estilo LBYL: comprobar antes
import os
if os.path.exists("datos.txt"):
    with open("datos.txt") as f:
        contenido = f.read()
else:
    contenido = ""

# estilo EAFP: intentar y manejar el fallo
try:
    with open("datos.txt") as f:
        contenido = f.read()
except FileNotFoundError:
    contenido = ""

Las dos formas producen el mismo resultado. La EAFP es preferida en Python por dos razones: evita una race condition (entre el exists() y el open() el archivo podría desaparecer) y suele ser más rápida cuando el caso de error es raro. La diferencia es pequeña en la práctica, pero conviene saber que la comunidad Python tiende al estilo "intenta y captura".

En la próxima lección: Python en la vida real: un panorama con fragmentos completos que muestran qué aspecto tiene el código cuando se usa para resolver problemas concretos.

TOP