Curso de Python · Bloque 06 de 09

Errores y excepciones

Captura, lanzamiento, excepciones propias y validación.

Audiencia. Personas que ya han visto algo de programación y quieren repasar Python de forma práctica.
Prerrequisitos. Funciones, condicionales y entrada/salida básica.
Duración orientativa. 60–90 minutos, incluida la práctica.

Objetivos de aprendizaje

  • Distinguir errores de sintaxis, de ejecución y de lógica.
  • Capturar únicamente las excepciones que se pueden gestionar.
  • Usar else, finally, raise y encadenamiento.
  • Definir excepciones propias y validar contratos.

Itinerario

  1. Tipos de fallo
  2. try/except
  3. else/finally
  4. Lanzar errores
  5. Excepciones propias
  6. EAFP
  7. Práctica

Cómo trabajar: ejecuta las celdas en orden, predice el resultado antes de verlo y modifica los ejemplos. Cada bloque termina con un ejercicio, un punto de partida y una solución posible.

1. Tres clases de fallo

  • Sintaxis: el código no se puede analizar.
  • Ejecución: ocurre una excepción al ejecutar (ValueError, TypeError, etc.).
  • Lógica: el programa corre, pero produce un resultado equivocado; las pruebas ayudan a detectarlo.

Capturamos una sintaxis inválida con compile para que el notebook continúe.

Python
codigo_invalido = "if True print('hola')"
try:
    compile(codigo_invalido, "<ejemplo>", "exec")
except SyntaxError as error:
    print(type(error).__name__, "→", error.msg)

try:
    int("doce")
except ValueError as error:
    print(type(error).__name__, "→", error)
Resultado
SyntaxError → invalid syntax
ValueError → invalid literal for int() with base 10: 'doce'

2. Captura específica con try y except

Coloca en try solo la operación que puede fallar. Captura excepciones concretas, no un except: indiscriminado: ocultaría interrupciones y errores de programación.

Python
def dividir_textos(numerador, denominador):
    try:
        a = float(numerador)
        b = float(denominador)
        return a / b
    except ValueError:
        return "entrada no numérica"
    except ZeroDivisionError:
        return "división por cero"

for caso in [("10", "2"), ("x", "2"), ("10", "0")]:
    print(caso, "→", dividir_textos(*caso))
Resultado
('10', '2') → 5.0
('x', '2') → entrada no numérica
('10', '0') → división por cero

Se pueden agrupar excepciones si comparten tratamiento. Guarda la excepción como error cuando el detalle sea útil; evita mostrar información sensible a usuarios finales.

Python
def obtener_elemento(valores, indice):
    try:
        return valores[int(indice)]
    except (ValueError, IndexError, TypeError) as error:
        return f"No disponible ({type(error).__name__})"

print(obtener_elemento(["a", "b"], "1"))
print(obtener_elemento(["a", "b"], "9"))
Resultado
b
No disponible (IndexError)

3. else y finally

else se ejecuta si no hubo excepción; evita ampliar innecesariamente el try. finally se ejecuta siempre y sirve para liberar recursos. Los gestores with suelen ser una alternativa más limpia para archivos y conexiones.

Python
def convertir_positivo(texto):
    try:
        valor = float(texto)
    except ValueError:
        resultado = "formato inválido"
    else:
        resultado = valor if valor > 0 else "no es positivo"
    finally:
        print(f"Intento terminado para {texto!r}")
    return resultado

print(convertir_positivo("3.5"))
print(convertir_positivo("tres"))
Resultado
Intento terminado para '3.5'
3.5
Intento terminado para 'tres'
formato inválido

4. Rechazar estados inválidos con raise

Una función debe fallar pronto y con un mensaje útil cuando se incumple su contrato. Usa tipos estándar cuando describen bien el problema: TypeError para tipo inadecuado y ValueError para valor fuera de dominio.

Python
def porcentaje(parte, total):
    if not isinstance(total, (int, float)):
        raise TypeError("total debe ser numérico")
    if total <= 0:
        raise ValueError("total debe ser mayor que cero")
    return parte / total * 100

print(porcentaje(25, 80))
try:
    porcentaje(2, 0)
except ValueError as error:
    print("Error controlado:", error)
Resultado
31.25
Error controlado: total debe ser mayor que cero

5. Excepciones propias y encadenamiento

Una excepción de dominio comunica mejor que un error genérico. Se hereda de Exception. raise ... from error conserva la causa original y añade contexto.

Python
class ConfiguracionError(Exception):
    '''Configuración ausente o inválida.'''

def leer_puerto(configuracion):
    try:
        puerto = int(configuracion["puerto"])
    except KeyError as error:
        raise ConfiguracionError("falta la clave 'puerto'") from error
    except ValueError as error:
        raise ConfiguracionError("el puerto debe ser entero") from error
    if not 1 <= puerto <= 65535:
        raise ConfiguracionError("puerto fuera de rango")
    return puerto

for config in [{"puerto": "8080"}, {"puerto": "abc"}, {}]:
    try:
        print("Puerto:", leer_puerto(config))
    except ConfiguracionError as error:
        print("Configuración rechazada:", error)
Resultado
Puerto: 8080
Configuración rechazada: el puerto debe ser entero
Configuración rechazada: falta la clave 'puerto'

6. EAFP, LBYL y aserciones

Python favorece a menudo EAFP (es más fácil pedir perdón): intenta la operación y captura el fallo esperado. LBYL (mira antes de saltar) comprueba primero. Las aserciones documentan invariantes internas, no validan datos de usuario: pueden desactivarse con optimización.

Python
perfil = {"nombre": "Sam", "idioma": "es"}

# EAFP: una sola búsqueda, con alternativa explícita.
try:
    zona = perfil["zona_horaria"]
except KeyError:
    zona = "UTC"

def media(valores):
    assert valores, "la colección interna no debería estar vacía"
    return sum(valores) / len(valores)

print(zona, media([8, 9, 10]))
Resultado
UTC 9.0

7. Registrar sin ocultar

En aplicaciones, logging es preferible a dispersar print: tiene niveles y destinos configurables. logger.exception se usa dentro de un except para incluir el traceback. Aquí mostramos un mensaje breve y controlado.

Python
import logging

logger = logging.getLogger("curso")
logger.setLevel(logging.INFO)
if not logger.handlers:
    logger.addHandler(logging.StreamHandler())

logger.info("El ejemplo de registro está activo")
Resultado
El ejemplo de registro está activo

Errores habituales

  • Atrapar Exception y continuar como si nada.
  • Cubrir demasiadas líneas con un solo try.
  • Usar excepciones para flujo normal que se puede expresar claramente.
  • Exponer mensajes internos o datos sensibles.
  • Usar assert para validar entradas externas.

Extensión opcional: investiga contextlib y la creación de gestores de contexto.

Práctica · Temperaturas tolerantes a errores

Implementa convertir_temperaturas(textos) que convierta Celsius a Fahrenheit. Debe devolver (validas, errores):

  • validas: lista de temperaturas convertidas;
  • errores: lista de diccionarios con entrada y motivo;
  • rechaza valores inferiores al cero absoluto (-273.15 °C).
Tu turno
def convertir_temperaturas(textos):
    validas = []
    errores = []
    # TODO: convierte cada entrada sin abortar todo el lote.
    return validas, errores
Ver una solución posible
Solución
def convertir_temperaturas(textos):
    validas = []
    errores = []
    for texto in textos:
        try:
            celsius = float(texto)
            if celsius < -273.15:
                raise ValueError("inferior al cero absoluto")
        except (TypeError, ValueError) as error:
            errores.append({"entrada": texto, "motivo": str(error)})
        else:
            validas.append(celsius * 9 / 5 + 32)
    return validas, errores

validas, errores = convertir_temperaturas(["0", "20.5", "frío", -300, None])
print("Válidas:", validas)
print("Errores:", errores)
assert validas == [32.0, 68.9] and len(errores) == 3
Resultado
Válidas: [32.0, 68.9]
Errores: [{'entrada': 'frío', 'motivo': "could not convert string to float: 'frío'"}, {'entrada': -300, 'motivo': 'inferior al cero absoluto'}, {'entrada': None, 'motivo': "float() argument must be a string or a real number, not 'NoneType'"}]

Comprobación rápida

  • [ ] Capturo excepciones concretas y cerca de su origen.
  • [ ] Sé cuándo usar else, finally y raise.
  • [ ] Puedo crear una excepción de dominio con una causa encadenada.

← Anterior · Volver al índice · Siguiente →