Errores y validaciones
En esta guía (12)
Hasta ahora, cuando algo salía mal (un int("hola"), una división por cero, una clave que no existe), el programa se cortaba con un traceback. En esta lección vas a aprender a manejar esos errores: atraparlos, decidir qué hacer, lanzar los tuyos cuando un dato no tiene sentido, y armar funciones que pidan datos al usuario hasta que sean válidos.
Excepciones
Cuando Python encuentra un problema mientras ejecuta, lanza una excepción. Si nadie la atrapa, el programa termina y muestra el traceback. Ya conocés varias:
| Excepción | Cuándo aparece | Ejemplo |
|---|---|---|
ValueError | el tipo es correcto pero el valor no sirve | int("hola") |
TypeError | la operación no vale para ese tipo | "a" + 1 |
ZeroDivisionError | división por cero | 10 / 0 |
IndexError | índice fuera de rango | [1, 2][5] |
KeyError | clave inexistente en un diccionario | {}["x"] |
NameError | variable no definida | print(variable_inexistente) |
FileNotFoundError | el archivo no existe | lo vas a ver en la lección 10 |
Los SyntaxError son distintos: aparecen antes de ejecutar, porque el código está mal escrito, y no se pueden atrapar. Se arreglan corrigiendo el código.
try / except
Ponés el código que puede fallar dentro de try, y en except qué hacer si falla:
texto = "doce"
try:
numero = int(texto)
print(f"El doble es {numero * 2}")
except ValueError:
print(f"'{texto}' no es un número entero")
print("El programa sigue")
'doce' no es un número entero
El programa sigue
Cómo funciona:
- Python ejecuta el bloque
trylínea por línea. - Si una línea lanza una excepción, deja de ejecutar el resto del
tryy salta alexceptque corresponda a ese tipo de error. - Si no hay error, el
exceptse saltea. - En cualquier caso, el programa sigue después del bloque.
Atrapá errores específicos
Siempre indicá qué excepción esperás. Podés tener varios except, uno por tipo:
def dividir(a_texto: str, b_texto: str) -> None:
try:
resultado = float(a_texto) / float(b_texto)
print(f"Resultado: {resultado}")
except ValueError:
print("Los dos valores tienen que ser números")
except ZeroDivisionError:
print("No se puede dividir por cero")
dividir("10", "4")
dividir("10", "cero")
dividir("10", "0")
Resultado: 2.5
Los dos valores tienen que ser números
No se puede dividir por cero
Si dos errores se manejan igual, se agrupan en una tupla: except (ValueError, TypeError):.
Nunca uses except: solo (ni except Exception: para todo, salvo casos muy puntuales). Atrapa absolutamente cualquier error, incluidos los que vienen de un bug tuyo, como un nombre de variable mal escrito. El programa no se corta, pero hace algo incorrecto en silencio y no te enterás por qué.
Ver el mensaje del error
Con as guardás la excepción en una variable para ver su mensaje:
try:
int("3,5")
except ValueError as e:
print(f"Error de conversión: {e}")
Error de conversión: invalid literal for int() with base 10: '3,5'
else y finally
El bloque try completo tiene dos partes opcionales más:
else: se ejecuta solo si no hubo error en eltry.finally: se ejecuta siempre, haya habido error o no. Se usa para tareas de cierre (liberar algo, dejar un registro).
def convertir(texto: str) -> None:
try:
numero = float(texto)
except ValueError:
print(f" '{texto}' no es válido")
else:
print(f" convertido: {numero}")
finally:
print(f" fin del intento con '{texto}'")
convertir("7.5")
convertir("siete")
convertido: 7.5
fin del intento con '7.5'
'siete' no es válido
fin del intento con 'siete'
¿Por qué usar else en vez de poner esa línea dentro del try? Para que el try quede chico, con solo la línea que puede fallar. Así el except no atrapa por accidente errores de otras líneas.
raise: lanzar tus propios errores
A veces el error no lo detecta Python sino vos: un dato que tiene el tipo correcto pero no tiene sentido para tu programa. Para eso está raise:
def aplicar_descuento(precio: float, porcentaje: float) -> float:
if precio < 0:
raise ValueError("El precio no puede ser negativo")
if not 0 <= porcentaje <= 100:
raise ValueError(f"Porcentaje inválido: {porcentaje}")
return precio * (1 - porcentaje / 100)
print(aplicar_descuento(1000, 20)) # 800.0
try:
aplicar_descuento(1000, 150)
except ValueError as e:
print(f"No se pudo aplicar: {e}")
800.0
No se pudo aplicar: Porcentaje inválido: 150
Lanzar un error es mejor que devolver un valor "raro" (como -1 o None) para avisar que algo falló: el error no se puede ignorar sin querer, y el mensaje explica qué pasó. La función se ocupa de detectar el problema; quien la llama decide qué hacer (mostrar un mensaje, pedir otro dato, cortar).
Validar el input del usuario en un bucle
Juntando while True, try y break podés pedir un dato hasta que sea válido:
def pedir_entero(mensaje: str, minimo: int, maximo: int) -> int:
"""Pide un entero entre minimo y maximo hasta que el usuario ingrese uno válido."""
while True:
texto = input(mensaje).strip()
try:
numero = int(texto)
except ValueError:
print("Tiene que ser un número entero.")
continue
if minimo <= numero <= maximo:
return numero
print(f"Tiene que estar entre {minimo} y {maximo}.")
edad = pedir_entero("Edad: ", 0, 120)
print(f"Edad registrada: {edad}")
Edad: veinte
Tiene que ser un número entero.
Edad: 200
Tiene que estar entre 0 y 120.
Edad: 34
Edad registrada: 34
El return dentro del while True corta el bucle y la función a la vez. Esta función la vas a reusar en el proyecto final: escribirla una vez y llamarla donde haga falta es mucho mejor que repetir el bucle en cada lugar donde pedís un número.
Comparado con el isdigit() de la lección 3, int() dentro de un try acepta negativos y espacios alrededor, y la misma idea sirve para float().
Excepciones propias
Para errores propios de tu programa podés crear tu propio tipo de excepción. Por ahora tomalo como una receta de dos líneas (la palabra class la vas a entender en la lección 11):
class SaldoInsuficienteError(Exception):
"""El saldo no alcanza para la operación pedida."""
def extraer(saldo: float, monto: float) -> float:
if monto <= 0:
raise ValueError("El monto tiene que ser positivo")
if monto > saldo:
raise SaldoInsuficienteError(f"Saldo {saldo}, se pidió {monto}")
return saldo - monto
saldo = 5000
for monto in [1500, -10, 9000]:
try:
saldo = extraer(saldo, monto)
print(f"Extracción de {monto} OK. Saldo: {saldo}")
except SaldoInsuficienteError as e:
print(f"Rechazada por saldo: {e}")
except ValueError as e:
print(f"Rechazada por dato inválido: {e}")
Extracción de 1500 OK. Saldo: 3500
Rechazada por dato inválido: El monto tiene que ser positivo
Rechazada por saldo: Saldo 3500, se pidió 9000
La ventaja: quien llama puede distinguir un problema de negocio (no hay saldo) de un dato mal cargado, y reaccionar distinto a cada uno. Por convención, el nombre termina en Error.
Errores comunes
except:pelado. Esconde bugs. Atrapá el tipo de error concreto que esperás.- Un
trygigante. Si envolvés 30 líneas, no sabés cuál falló. Dejá en eltrysolo lo que puede lanzar el error que manejás. - Tragarse el error. Un
except ValueError: passhace que el problema desaparezca sin dejar rastro. Como mínimo, mostrá un mensaje. - Usar excepciones para lo que se resuelve con un
if. Para una clave que puede faltar,dic.get(clave)es más claro que untry/except KeyError. - Validar y no volver a pedir. Si mostrás "dato inválido" pero seguís con el valor malo, el error aparece más adelante y es más difícil de rastrear. Volvé a pedir o cortá.
- Confundir el mensaje con el tipo.
except "division by zero":no funciona; se atrapa por tipo (ZeroDivisionError).
Practicá
- Escribí
pedir_decimal(mensaje: str) -> floatque pida un número con decimales hasta que sea válido, aceptando tanto12.5como12,5. - Tenés
lecturas = ["21.5", "22", "error", "", "19.8", "N/A", "23.1"](lecturas de un sensor). Convertí las válidas afloat, contá cuántas fallaron y mostrá el promedio de las válidas. - Escribí
validar_email(email: str) -> strque devuelva el email en minúsculas y sin espacios, o lanceValueErrorcon un mensaje claro si no tiene exactamente una@, o si no hay un punto después de la@. Probala con varios casos dentro de untry. - Escribí
obtener_precio(catalogo: dict[str, float], producto: str) -> floatque lance una excepción propiaProductoNoEncontradoErrorsi el producto no está. Usala en un bucle que pida productos hasta que el usuario escribafin, y mostrá el precio o un mensaje de error.
Soluciones
Ejercicio 1:
def pedir_decimal(mensaje: str) -> float:
"""Pide un número decimal; acepta coma o punto como separador."""
while True:
texto = input(mensaje).strip().replace(",", ".")
try:
return float(texto)
except ValueError:
print("Ingresá un número, por ejemplo 12.5")
precio = pedir_decimal("Precio: ")
print(f"Precio: {precio:.2f}")
Precio: doce
Ingresá un número, por ejemplo 12.5
Precio: 12,5
Precio: 12.50
Ejercicio 2:
lecturas = ["21.5", "22", "error", "", "19.8", "N/A", "23.1"]
validas = []
fallidas = 0
for lectura in lecturas:
try:
validas.append(float(lectura))
except ValueError:
fallidas += 1
print(f"Válidas: {len(validas)}, fallidas: {fallidas}")
print(f"Promedio: {sum(validas) / len(validas):.2f}")
Válidas: 4, fallidas: 3
Promedio: 21.60
Ejercicio 3:
def validar_email(email: str) -> str:
"""Normaliza el email o lanza ValueError si no tiene un formato básico válido."""
email = email.strip().lower()
if email.count("@") != 1:
raise ValueError("Tiene que tener exactamente una @")
usuario, dominio = email.split("@")
if not usuario:
raise ValueError("Falta el usuario antes de la @")
if "." not in dominio:
raise ValueError("El dominio tiene que tener un punto")
return email
for prueba in [" Ana@Correo.com ", "ana.correo.com", "ana@correo", "@correo.com"]:
try:
print(f"OK: {validar_email(prueba)}")
except ValueError as e:
print(f"'{prueba}': {e}")
OK: ana@correo.com
'ana.correo.com': Tiene que tener exactamente una @
'ana@correo': El dominio tiene que tener un punto
'@correo.com': Falta el usuario antes de la @
Ejercicio 4:
class ProductoNoEncontradoError(Exception):
"""El producto pedido no está en el catálogo."""
def obtener_precio(catalogo: dict[str, float], producto: str) -> float:
if producto not in catalogo:
raise ProductoNoEncontradoError(f"No tenemos '{producto}'")
return catalogo[producto]
catalogo = {"yerba": 3200, "café": 4500, "té": 1800}
while True:
producto = input("Producto (fin para salir): ").strip().lower()
if producto == "fin":
break
try:
print(f"Precio: ${obtener_precio(catalogo, producto)}")
except ProductoNoEncontradoError as e:
print(e)
Producto (fin para salir): café
Precio: $4500
Producto (fin para salir): mate
No tenemos 'mate'
Producto (fin para salir): fin
Resumen
- Las excepciones son errores que aparecen al ejecutar; si nadie las atrapa, cortan el programa.
try/except Tipo:atrapa un error concreto;as eda acceso al mensaje.elsecorre si no hubo error;finallycorre siempre.- Nunca uses
except:pelado, y mantené eltrychico. raise ValueError("mensaje")lanza un error cuando un dato no tiene sentido para tu programa.while True+try+returnarma funciones que piden datos hasta que sean válidos.class MiError(Exception): ...crea una excepción propia para distinguir errores de tu dominio.