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,raisey encadenamiento. - Definir excepciones propias y validar contratos.
Itinerario
- Tipos de fallo
try/exceptelse/finally- Lanzar errores
- Excepciones propias
- EAFP
- 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.
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)
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.
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))
('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.
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"))
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.
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"))
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.
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)
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.
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)
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.
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]))
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.
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")
El ejemplo de registro está activo
Errores habituales
- Atrapar
Exceptiony 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
assertpara 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).
def convertir_temperaturas(textos):
validas = []
errores = []
# TODO: convierte cada entrada sin abortar todo el lote.
return validas, errores
Ver una solución posible
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
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,finallyyraise. - [ ] Puedo crear una excepción de dominio con una causa encadenada.