trubidata
Lección 8 de 12·20 min

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ónCuándo apareceEjemplo
ValueErrorel tipo es correcto pero el valor no sirveint("hola")
TypeErrorla operación no vale para ese tipo"a" + 1
ZeroDivisionErrordivisión por cero10 / 0
IndexErroríndice fuera de rango[1, 2][5]
KeyErrorclave inexistente en un diccionario{}["x"]
NameErrorvariable no definidaprint(variable_inexistente)
FileNotFoundErrorel archivo no existelo 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:

python
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")
text
'doce' no es un número entero
El programa sigue

Cómo funciona:

  1. Python ejecuta el bloque try línea por línea.
  2. Si una línea lanza una excepción, deja de ejecutar el resto del try y salta al except que corresponda a ese tipo de error.
  3. Si no hay error, el except se saltea.
  4. 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:

python
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")
text
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:

python
try:
    int("3,5")
except ValueError as e:
    print(f"Error de conversión: {e}")
text
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 el try.
  • finally: se ejecuta siempre, haya habido error o no. Se usa para tareas de cierre (liberar algo, dejar un registro).
python
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")
text
  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:

python
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}")
text
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:

python
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}")
text
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):

python
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}")
text
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 try gigante. Si envolvés 30 líneas, no sabés cuál falló. Dejá en el try solo lo que puede lanzar el error que manejás.
  • Tragarse el error. Un except ValueError: pass hace 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 un try/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á

  1. Escribí pedir_decimal(mensaje: str) -> float que pida un número con decimales hasta que sea válido, aceptando tanto 12.5 como 12,5.
  2. Tenés lecturas = ["21.5", "22", "error", "", "19.8", "N/A", "23.1"] (lecturas de un sensor). Convertí las válidas a float, contá cuántas fallaron y mostrá el promedio de las válidas.
  3. Escribí validar_email(email: str) -> str que devuelva el email en minúsculas y sin espacios, o lance ValueError con un mensaje claro si no tiene exactamente una @, o si no hay un punto después de la @. Probala con varios casos dentro de un try.
  4. Escribí obtener_precio(catalogo: dict[str, float], producto: str) -> float que lance una excepción propia ProductoNoEncontradoError si el producto no está. Usala en un bucle que pida productos hasta que el usuario escriba fin, y mostrá el precio o un mensaje de error.

Soluciones

Ejercicio 1:

python
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}")
text
Precio: doce
Ingresá un número, por ejemplo 12.5
Precio: 12,5
Precio: 12.50

Ejercicio 2:

python
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}")
text
Válidas: 4, fallidas: 3
Promedio: 21.60

Ejercicio 3:

python
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}")
text
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:

python
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)
text
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 e da acceso al mensaje.
  • else corre si no hubo error; finally corre siempre.
  • Nunca uses except: pelado, y mantené el try chico.
  • raise ValueError("mensaje") lanza un error cuando un dato no tiene sentido para tu programa.
  • while True + try + return arma funciones que piden datos hasta que sean válidos.
  • class MiError(Exception): ... crea una excepción propia para distinguir errores de tu dominio.

← Volver al temario