Excepciones en Python — práctica con cuatro programas que no se rompen: una ficha de matrícula, una calculadora, un lector de notas y fracciones (y el «12,5» que no quería ser número)
Cuando monté mi primera ficha de matrícula «a prueba de todo», le pasé el programa a una compañera para que lo rompiera. Tardó diez segundos: en la nota de acceso escribió 12,5, con coma, y el programa le soltó un could not convert string to float en inglés, sin explicación. No se rompió (lo había metido en un try), pero tampoco le dijo nada útil. Esa es la lección de hoy: un programa robusto no es solo el que no se para; es el que sabe qué ha fallado y lo dice.
En la teoría de excepciones en Python viste try, except, else, finally, raise y las excepciones propias. Hoy las usamos en cuatro programas completos, todos de nivel medio: una ficha de matrícula que insiste hasta que los datos valen, una calculadora que aguanta cualquier cosa que escribas, un lector de notas que lee un fichero con líneas estropeadas y una clase de fracciones con su propia familia de excepciones. Los datos se parecen a los de verdad, con huecos, comas, textos donde van números y ficheros que no existen, porque es justo ahí donde las excepciones demuestran para qué sirven.
Tabla de Contenidos
Antes de empezar
Conviene tener claro:
- Que, cuando una línea del
tryfalla, el resto deltryse salta y se entra en el primerexceptque encaja. - La diferencia entre
else(solo si no hubo excepción) yfinally(siempre). - Que
raise ValueError('mensaje')lanza una excepción tuya y que elexcept ValueErrorde quien te llamó la captura. - Cómo se crea una excepción propia:
class MiError(Exception):.
Si alguno te suena dudoso, repasa la teoría antes de seguir.
Ninguno de los programas usa input(): lo que teclearía la persona va en una lista (TECLEO) y la función leer() lo imprime como saldría en la terminal, para que veas salidas reales con datos buenos y malos. En tu ordenador, borra TECLEO y leer() y escribe input(...) donde pone leer(...).
Excepciones en Python práctica — Programa 1: Validador robusto de datos
La secretaría quiere una ficha de matrícula: nombre, edad (de 16 a 99), nota de acceso (de 5 a 14) y modalidad (presencial u online). Lo que la complica es que cada dato puede fallar de dos maneras: no ser del tipo correcto ('doce' no es un entero) o serlo pero estar fuera de rango (12 años). El programa tiene que volver a pedir el dato hasta que valga, sin romperse nunca.
La idea
Una función por tipo de dato (pedir_entero, pedir_decimal, pedir_opcion), todas con el mismo esquema: un while True con un try dentro. Si el dato vale, return (y eso sale del bucle). Si no, el except avisa y el bucle da otra vuelta. El truco está en el rango: en vez de un if con su propio print, lanzamos nosotros un ValueError, para que los dos fallos (el de int() y el del rango) acaben en el mismo except.
Antes de programar, piensa en cuántas formas tiene cada dato de estar mal. Para la edad, dos (no es número, está fuera de rango). Para la modalidad, una sola (no es ninguna de las opciones), así que ahí no hay conversión que pueda fallar.
El código
# Lo que teclearía la persona (en tu ordenador, cambia leer() por input()):
TECLEO = iter(['Laura Pérez', 'doce', '12', '22', '15', '12,5', '12.5', 'virtual', 'Presencial'])
def leer(mensaje):
texto = next(TECLEO) # el siguiente dato «tecleado»
print(mensaje + texto) # lo enseñamos como se vería en la terminal
return texto
def pedir_entero(mensaje, minimo, maximo):
while True: # insiste hasta que el dato valga
try:
valor = int(leer(mensaje)) # ValueError si no es un entero
if not minimo <= valor <= maximo:
raise ValueError(f'tiene que estar entre {minimo} y {maximo}')
return valor # dato bueno: salimos del bucle
except ValueError as err: # los dos casos acaban aquí
print(f' Error: {err}')
def pedir_decimal(mensaje, minimo, maximo):
while True:
try:
valor = float(leer(mensaje))
if not minimo <= valor <= maximo:
raise ValueError(f'tiene que estar entre {minimo} y {maximo}')
return valor
except ValueError as err:
print(f' Error: {err}')
def pedir_opcion(mensaje, opciones):
while True:
try:
opcion = leer(mensaje).strip().lower() # ' Presencial' -> 'presencial'
if opcion not in opciones:
raise ValueError(f'elige una de estas: {", ".join(opciones)}')
return opcion
except ValueError as err:
print(f' Error: {err}')
print('=== FICHA DE MATRÍCULA ===')
nombre = leer('Nombre: ').strip() or 'Sin nombre'
edad = pedir_entero('Edad (16-99): ', 16, 99)
nota = pedir_decimal('Nota de acceso (5-14): ', 5.0, 14.0)
modalidad = pedir_opcion('Modalidad (presencial/online): ', ['presencial', 'online'])
print('--- Ficha guardada ---')
print(f'Nombre: {nombre}')
print(f'Edad: {edad} años')
print(f'Nota: {nota:.2f}')
print(f'Modalidad: {modalidad}')
Vamos a hacer zoom a la función que lleva todo el peso:
def pedir_entero(mensaje, minimo, maximo):
while True: # insiste hasta que el dato valga
try:
valor = int(leer(mensaje)) # ValueError si no es un entero
if not minimo <= valor <= maximo:
raise ValueError(f'tiene que estar entre {minimo} y {maximo}')
return valor # dato bueno: salimos del bucle
except ValueError as err: # los dos casos acaban aquí
print(f' Error: {err}')
El while True solo tiene una salida: el return valor, que únicamente se alcanza si las dos comprobaciones anteriores han pasado. int(leer(mensaje)) lanza ValueError si el texto no es un entero; la comparación encadenada minimo <= valor <= maximo comprueba el rango y, si falla, raise ValueError(...) lanza el nuestro. Los dos caen en except ValueError as err y se imprime err, que es el mensaje de cada uno.
Salida
=== FICHA DE MATRÍCULA === Nombre: Laura Pérez Edad (16-99): doce Error: invalid literal for int() with base 10: 'doce' Edad (16-99): 12 Error: tiene que estar entre 16 y 99 Edad (16-99): 22 Nota de acceso (5-14): 15 Error: tiene que estar entre 5.0 y 14.0 Nota de acceso (5-14): 12,5 Error: could not convert string to float: '12,5' Nota de acceso (5-14): 12.5 Modalidad (presencial/online): virtual Error: elige una de estas: presencial, online Modalidad (presencial/online): Presencial --- Ficha guardada --- Nombre: Laura Pérez Edad: 22 años Nota: 12.50 Modalidad: presencial
Comprobemos cada vuelta del bucle de la edad con una traza:
| Vuelta | Tecleo | int() | ¿16 ≤ valor ≤ 99? | Qué pasa |
|---|---|---|---|---|
| 1 | 'doce' | lanza ValueError | (no se llega) | Mensaje de Python, otra vuelta |
| 2 | '12' | 12 | No: 12 < 16 | Nuestro raise, otra vuelta |
| 3 | '22' | 22 | Sí | return 22, se sale del bucle |
La nota tiene el mismo esquema con float(): 15 se pasa de 14, 12,5 no es un número para Python (usa punto decimal) y 12.5 entra. La modalidad acepta Presencial porque .strip().lower() la deja en presencial antes de comprobarla.
El diagrama es el mismo bucle visto desde arriba: dos flechas rojas que vuelven a leer() y una sola salida verde.
Preguntas que te puedes hacer
¿Por qué lanzar un ValueError para el rango, si podría poner un print y un continue? Funcionaría, pero tendrías dos sitios que tratan errores. Con el raise, todo lo que hace que el dato no valga pasa por el mismo except y el bucle es más corto. Además, si mañana quieres registrar los fallos en un fichero, solo cambias una línea.
¿Por qué el try está dentro del while y no fuera? Si el try envolviera el bucle entero, el primer error te sacaría del bucle y ya no se volvería a pedir el dato. Dentro, cada vuelta tiene su propio try: un fallo solo estropea esa vuelta.
¿Por qué el mensaje de 'doce' sale en inglés? Porque es el de Python (invalid literal for int() with base 10). Imprimimos err tal cual. Es lo que le pasó a mi compañera con el 12,5. En el «Pruébalo tú» lo cambias por uno en español.
¿Por qué pedir_opcion tiene un try si nada en ella lanza excepciones de Python? Porque nuestro propio raise ValueError sí la lanza. El try atrapa tanto excepciones de Python como las tuyas: para él son iguales.
¿Qué pasa si el nombre llega vacío? leer('Nombre: ').strip() daría '', que es falso, y or 'Sin nombre' pone un valor por defecto. Aquí no hace falta excepción: no hay nada que «falle», solo un dato que falta.
Poner el return valor fuera del try, después del except. Si int() falla, valor no llega a existir y el return lanzaría UnboundLocalError. Devuelve el valor dentro del try, justo cuando sabes que es bueno.
Pruébalo tú
- Haz que
pedir_decimalacepte la coma: cambiafloat(leer(mensaje))porfloat(leer(mensaje).replace(',', '.'))y comprueba que12,5entra a la primera. - Separa los dos mensajes: captura el
ValueErrordeint()en untryinterior para imprimir «la edad tiene que ser un número entero» en español, y deja elraisedel rango con su mensaje.
Excepciones en Python práctica — Programa 2: Calculadora que no se rompe
Una calculadora de una línea: escribes 10 / 3 y responde = 3.333…, hasta que escribes salir. Lo que la complica es que cualquier cosa puede llegar: números que no son números, operadores que no existen, líneas a medias, divisiones entre cero y resultados tan grandes que no caben en un float. Cada error tiene que salir con su propio mensaje y la calculadora tiene que seguir funcionando.
La idea
Separamos las responsabilidades. calcular(linea) solo calcula: trocea la línea, comprueba el formato y el operador y, si algo no cuadra, lanza (raise). No imprime nada. El bucle principal es el único que captura, con un except por cada familia de error. Las operaciones están en un diccionario de funciones: así no necesitamos un if por operador.
Antes de programar, apunta qué errores lanza Python solo y cuáles tienes que lanzar tú. Dividir entre cero o un float('diez') los lanza Python. Que la línea tenga tres trozos o que ^ no sea un operador válido, no: esos son reglas tuyas.
El código
# Lo que teclearía la persona (en tu ordenador, cambia leer() por input()):
TECLEO = iter(['10 / 0', 'diez + 3', '10 ^ 3', '8 //', '2 ** 10000', '7 % 0', '2 ** 10', 'salir'])
def leer(mensaje):
texto = next(TECLEO) # el siguiente dato «tecleado»
print(mensaje + texto) # lo enseñamos como se vería en la terminal
return texto
OPERACIONES = { # símbolo -> función que hace la cuenta
'+': lambda a, b: a + b,
'-': lambda a, b: a - b,
'*': lambda a, b: a * b,
'/': lambda a, b: a / b,
'//': lambda a, b: a // b,
'%': lambda a, b: a % b,
'**': lambda a, b: a ** b,
}
def calcular(linea):
partes = linea.split()
if len(partes) != 3: # «8 //» solo tiene dos trozos
raise ValueError('formato: número operación número')
a, op, b = partes
if op not in OPERACIONES:
raise ValueError(f'operación desconocida: {op}')
return OPERACIONES[op](float(a), float(b)) # float() también puede fallar
print('=== CALCULADORA (escribe «salir» para terminar) ===')
while True:
linea = leer('> ')
if linea == 'salir':
print('Hasta luego')
break
try:
print(' =', calcular(linea))
except ZeroDivisionError as err: # la lanza Python, no nosotros
print(' Error matemático:', err)
except OverflowError as err: # resultado demasiado grande para un float
print(' Demasiado grande:', err)
except ValueError as err: # nuestros raise y los de float()
print(' Error:', err)
La función que decide si la línea vale:
def calcular(linea):
partes = linea.split()
if len(partes) != 3: # «8 //» solo tiene dos trozos
raise ValueError('formato: número operación número')
a, op, b = partes
if op not in OPERACIONES:
raise ValueError(f'operación desconocida: {op}')
return OPERACIONES[op](float(a), float(b)) # float() también puede fallar
linea.split() corta por los espacios: '10 / 0' da ['10', '/', '0']. Si no son tres trozos, raise ValueError. Si el operador no está en OPERACIONES, otro raise ValueError. Y la última línea hace tres cosas que pueden fallar: float(a), float(b) y la propia operación. Ninguna está protegida aquí, a propósito: suben hasta el bucle.
Salida
=== CALCULADORA (escribe «salir» para terminar) === > 10 / 0 Error matemático: float division by zero > diez + 3 Error: could not convert string to float: 'diez' > 10 ^ 3 Error: operación desconocida: ^ > 8 // Error: formato: número operación número > 2 ** 10000 Demasiado grande: (34, 'Numerical result out of range') > 7 % 0 Error matemático: float modulo by zero > 2 ** 10 = 1024.0 > salir Hasta luego
Cada línea acaba en un except distinto:
| Tecleo | Quién lanza | Excepción | except que la captura |
|---|---|---|---|
10 / 0 | Python, en a / b | ZeroDivisionError | Error matemático |
diez + 3 | Python, en float('diez') | ValueError | Error |
10 ^ 3 | Nosotros: operador desconocido | ValueError | Error |
8 // | Nosotros: dos trozos | ValueError | Error |
2 ** 10000 | Python: no cabe en un float | OverflowError | Demasiado grande |
7 % 0 | Python, en a % b | ZeroDivisionError | Error matemático |
2 ** 10 | nadie | ninguna | Resultado: 2¹⁰ = 1024.0 |
Fíjate en los mensajes de ZeroDivisionError: cambian según la operación (float division by zero, float modulo by zero). Los escribe Python, y por eso los imprimimos con err en vez de inventar uno.
Preguntas que te puedes hacer
¿Por qué no comprobamos b == 0 antes de dividir, como en el artículo antiguo? Porque Python ya lanza ZeroDivisionError él solo, con un mensaje correcto, en /, // y %. Repetir esa comprobación a mano es código que puede equivocarse (olvidarte del %, por ejemplo) sin aportar nada.
¿Por qué OverflowError tiene su propio except y no va con ZeroDivisionError? Porque las dos son hijas de ArithmeticError y podríamos capturarlas juntas con except ArithmeticError, pero el mensaje para el usuario es distinto: no es lo mismo «no se puede dividir entre cero» que «el número es demasiado grande».
¿Por qué calcular no tiene ningún try? Porque no sabe qué hacer con el error: imprimirlo es cosa del bucle. Si mañana usas calcular desde una interfaz gráfica, querrás mostrar el error en una ventana, no en la terminal. La función lanza; quien la usa decide.
¿El orden de los except importa aquí? Entre estos tres no, porque ninguno es madre de otro (ValueError no es hija de ArithmeticError). Pero si añadieras except ArithmeticError antes de except ZeroDivisionError, este último no se ejecutaría nunca.
Meter el print del resultado y el calcular en el mismo try junto con más código (leer, comprobar salir…). Cuantas más líneas haya en el try, más difícil es saber qué lanzó el ValueError. Aquí el try tiene una sola línea.
Pruébalo tú
- Añade la raíz cuadrada con
'raiz'como operador de un solo número (raiz 16): tendrás que aceptar dos trozos para ese caso y lanzarValueErrorsi el número es negativo. - Captura también
KeyboardInterrupten el bucle para que Ctrl+C imprima «Hasta luego» en vez del Traceback.
Excepciones en Python práctica — Programa 3: Lector de ficheros con gestión de errores
La secretaría guarda las notas de un grupo en un fichero de texto, una por línea. El fichero real nunca está limpio: hay líneas vacías, alguien escribió ocho con letras y hay un 11 que no es una nota válida. Además, el fichero puede no existir (el acta de 2027 todavía no está). El programa tiene que leer lo que pueda, avisar de lo que no y, pase lo que pase, cerrar el fichero.
La idea
Dos niveles de try. El exterior rodea todo el trabajo con el fichero: captura que no exista (FileNotFoundError) o que no tengamos permiso (PermissionError) y, en su finally, lo cierra. El interior va dentro del bucle y rodea solo la conversión de cada línea: si una línea está mal, se avisa y se pasa a la siguiente, sin interrumpir la lectura. Para que el finally pueda preguntar si hay algo que cerrar, fichero empieza valiendo None.
Antes de programar, separa los errores que afectan a todo el fichero (no existe) de los que afectan a una sola línea ('ocho'). Cada grupo va en un try a su altura.
El código
# Preparamos el fichero de ejemplo (en la secretaría ya existiría)
with open('notas.txt', 'w', encoding='utf-8') as f:
f.write('8.5\nocho\n11\n6\n\n9.5\n4\n7\n')
def leer_notas(nombre_fichero):
fichero = None # así existe aunque open() falle
try:
fichero = open(nombre_fichero, encoding='utf-8')
notas = []
for num, linea in enumerate(fichero, start=1):
linea = linea.strip()
if not linea:
continue # línea vacía: ni error ni nota
try: # try DENTRO del bucle: una línea mala no para la lectura
nota = float(linea)
except ValueError:
print(f' Línea {num} ignorada: {linea!r} no es un número')
continue
if not 0 <= nota <= 10:
print(f' Línea {num} ignorada: {nota} está fuera de rango')
continue
notas.append(nota)
return notas
except FileNotFoundError:
print(f' Error: no existe el fichero {nombre_fichero!r}')
return []
except PermissionError:
print(f' Error: sin permiso para leer {nombre_fichero!r}')
return []
finally: # se ejecuta con return, con error y sin error
if fichero is not None:
fichero.close()
print(' (fichero cerrado)')
def informe(notas):
if not notas:
print(' No hay notas que analizar')
return
aprobados = sum(1 for n in notas if n >= 5)
print(f' {len(notas)} notas | media {sum(notas) / len(notas):.2f} | '
f'máx {max(notas)} | mín {min(notas)} | aprobados {aprobados}/{len(notas)}')
for nombre in ['notas.txt', 'notas_2027.txt']:
print('Leyendo', nombre)
informe(leer_notas(nombre))
El corazón del programa es el bucle, con su try interior:
for num, linea in enumerate(fichero, start=1):
linea = linea.strip()
if not linea:
continue # línea vacía: ni error ni nota
try: # try DENTRO del bucle: una línea mala no para la lectura
nota = float(linea)
except ValueError:
print(f' Línea {num} ignorada: {linea!r} no es un número')
continue
if not 0 <= nota <= 10:
print(f' Línea {num} ignorada: {nota} está fuera de rango')
continue
notas.append(nota)
enumerate(fichero, start=1) da el número de línea (empezando en 1, como lo cuenta una persona) junto con su texto. El try solo rodea float(linea): es lo único que puede fallar. El rango se comprueba con un if normal, porque aquí no hace falta lanzar nada: basta con avisar y seguir.
Salida
Leyendo notas.txt Línea 2 ignorada: 'ocho' no es un número Línea 3 ignorada: 11.0 está fuera de rango (fichero cerrado) 5 notas | media 7.00 | máx 9.5 | mín 4.0 | aprobados 4/5 Leyendo notas_2027.txt Error: no existe el fichero 'notas_2027.txt' No hay notas que analizar
Traza del bucle sobre notas.txt:
| Línea | Texto | Qué pasa | notas después |
|---|---|---|---|
| 1 | 8.5 | Se guarda | [8.5] |
| 2 | ocho | ValueError, se ignora | [8.5] |
| 3 | 11 | Fuera de rango, se ignora | [8.5] |
| 4 | 6 | Se guarda | [8.5, 6.0] |
| 5 | (vacía) | continue sin aviso | [8.5, 6.0] |
| 6 | 9.5 | Se guarda | [8.5, 6.0, 9.5] |
| 7 | 4 | Se guarda | [8.5, 6.0, 9.5, 4.0] |
| 8 | 7 | Se guarda | [8.5, 6.0, 9.5, 4.0, 7.0] |
Comprobación: 8,5 + 6 + 9,5 + 4 + 7 = 35, y 35 / 5 = 7,00. Aprueban todas menos el 4: 4/5. Fíjate en que «(fichero cerrado)» sale antes del informe: el finally se ejecuta al salir de leer_notas, justo antes de que el return notas entregue la lista. Con notas_2027.txt, open() falla, fichero sigue siendo None y el finally no tiene nada que cerrar.
Preguntas que te puedes hacer
¿Por qué fichero = None antes del try? Porque si open() falla, la variable fichero no llegaría a crearse y el finally daría UnboundLocalError al intentar fichero.close(). En los ejercicios lo vas a ver fallar de verdad.
¿No es más fácil usar with open(...)? Sí, y es lo que harás casi siempre: with cierra el fichero solo al terminar el bloque, incluso si hay una excepción. Aquí lo escribimos con finally para que veas qué hace with por debajo. En la agenda de los ejercicios lo usamos con with.
¿Por qué un if para el rango y no un raise? Porque estamos en el mismo sitio donde se trata el error. Lanzar una excepción para capturarla dos líneas más abajo sería dar un rodeo. raise tiene sentido cuando el que detecta y el que trata son funciones distintas, como en el programa 1.
¿Qué pasaría si el try interior no existiera? La línea 2 (ocho) lanzaría ValueError, que no es FileNotFoundError ni PermissionError, así que el try exterior no la capturaría: el finally cerraría el fichero y el programa se pararía con el Traceback. Cinco notas válidas perdidas por una línea mala.
Capturar except Exception en el try exterior «para que no falle nunca». Una línea mala acabaría ahí, se devolvería una lista vacía y no sabrías que el fichero tenía cinco notas buenas.
Pruébalo tú
- Reescribe
leer_notasconwith open(...) as fichero:y comprueba que la salida es la misma (salvo el aviso «fichero cerrado», que ya no hace falta). - Cuenta cuántas líneas se ignoraron y devuélvelo junto con las notas:
return notas, ignoradas.
Excepciones en Python práctica — Programa 4: Clase con excepciones propias: números racionales
Una clase Racional para fracciones como 3/4. Lo que la complica es que una fracción con denominador 0 no existe y que solo tiene sentido sumarla con otra fracción. Queremos que la propia clase lo impida lanzando sus propias excepciones, organizadas en una familia, y que el programa principal pida los datos hasta tener dos fracciones válidas y las sume. Usamos una clase porque es como se hace en FP2; si todavía no las has visto, quédate con la idea y vuelve después del siguiente tema, clases en Python.
La idea
Tres excepciones: ErrorRacional como madre, y ErrorDenominadorCero y ErrorTipoIncorrecto como hijas. El constructor __init__ comprueba los datos y lanza la que toque; si todo va bien, guarda la fracción simplificada con el máximo común divisor (gcd). El programa principal captura la hija concreta cuando quiere un mensaje concreto y la madre cuando le vale cualquiera de la familia.
Antes de programar, dibuja el árbol de excepciones en un papel. Si tienes claro quién es madre de quién, el orden de los except sale solo.
El código
# Lo que teclearía la persona (en tu ordenador, cambia leer() por input()):
TECLEO = iter(['3', '0', 'tres', '3', '4', '1', '2'])
def leer(mensaje):
texto = next(TECLEO) # el siguiente dato «tecleado»
print(mensaje + texto) # lo enseñamos como se vería en la terminal
return texto
from math import gcd
class ErrorRacional(Exception):
"""Base de los errores de los números racionales."""
class ErrorDenominadorCero(ErrorRacional):
"""El denominador vale 0."""
class ErrorTipoIncorrecto(ErrorRacional):
"""Numerador o denominador no son enteros."""
class Racional:
def __init__(self, num, den):
if not (isinstance(num, int) and isinstance(den, int)):
raise ErrorTipoIncorrecto('numerador y denominador deben ser enteros')
if den == 0:
raise ErrorDenominadorCero('el denominador no puede ser cero')
m = gcd(num, den) # simplificamos: 10/8 -> 5/4
self.num, self.den = num // m, den // m
def __str__(self):
return f'{self.num}/{self.den}'
def __add__(self, otro): # r1 + r2 llama a r1.__add__(r2)
if not isinstance(otro, Racional):
raise ErrorTipoIncorrecto('solo se suman racionales')
return Racional(self.num * otro.den + otro.num * self.den, self.den * otro.den)
def pedir_racional(n):
num = int(leer(f'Numerador {n}: ')) # ValueError si no es un entero
den = int(leer(f'Denominador {n}: '))
return Racional(num, den) # ErrorRacional si no vale
print('=== SUMA DE RACIONALES ===')
while True:
try:
r1 = pedir_racional(1)
r2 = pedir_racional(2)
break
except ErrorDenominadorCero as err:
print(' Error de racional:', err)
except ValueError:
print(' Error: escribe números enteros')
print(f'{r1} + {r2} = {r1 + r2}')
try:
print(r1 + 0.5)
except ErrorRacional as err: # la base captura a cualquier hija
print(f' {type(err).__name__}: {err}')
El constructor es el guardián: ninguna fracción mala llega a existir.
def __init__(self, num, den):
if not (isinstance(num, int) and isinstance(den, int)):
raise ErrorTipoIncorrecto('numerador y denominador deben ser enteros')
if den == 0:
raise ErrorDenominadorCero('el denominador no puede ser cero')
m = gcd(num, den) # simplificamos: 10/8 -> 5/4
self.num, self.den = num // m, den // m
Si num o den no son enteros, ErrorTipoIncorrecto; si den es 0, ErrorDenominadorCero. Solo si pasa las dos comprobaciones se calcula gcd y se guardan los atributos. Así, cualquier Racional que exista en tu programa es válido: no hace falta volver a comprobarlo en cada método.
Salida
=== SUMA DE RACIONALES === Numerador 1: 3 Denominador 1: 0 Error de racional: el denominador no puede ser cero Numerador 1: tres Error: escribe números enteros Numerador 1: 3 Denominador 1: 4 Numerador 2: 1 Denominador 2: 2 3/4 + 1/2 = 5/4 ErrorTipoIncorrecto: solo se suman racionales
La suma, a mano: 3/4 + 1/2 = (3·2 + 1·4) / (4·2) = 10/8, y como gcd(10, 8) = 2, queda 5/4. El artículo antiguo enseñaba 10/8 porque no simplificaba.
El diagrama sigue el primer intento: Racional(3, 0) lanza ErrorDenominadorCero dentro de __init__; pedir_racional no tiene try, así que la excepción la atraviesa y la captura el except del bucle, que vuelve a pedir los datos. El segundo intento falla antes, en int('tres'), y lo captura except ValueError.
Preguntas que te puedes hacer
¿Por qué la última suma (r1 + 0.5) la captura except ErrorRacional si lanza ErrorTipoIncorrecto? Porque ErrorTipoIncorrecto es hija de ErrorRacional. Capturar la madre es la forma de decir «cualquier problema con fracciones», y type(err).__name__ nos dice cuál ha sido.
¿Por qué en el bucle no capturamos ErrorTipoIncorrecto? Porque ahí no puede ocurrir: int() siempre devuelve un entero o lanza ValueError. Capturar algo que no puede pasar solo confunde a quien lee el código.
¿Para qué sirve la clase madre si nunca la lanzamos? Para capturar en grupo. Un programa que use tu clase puede escribir un solo except ErrorRacional y quedarse tranquilo con cualquier error de fracciones, sin conocer todas las hijas. Además, si mañana añades ErrorSimplificacion, ese except ya la cubre.
¿Y __add__? Es el método que Python llama cuando escribes r1 + r2. Lo verás a fondo en el tema de sobrecarga de operadores; aquí te basta con saber que también puede lanzar excepciones como cualquier otro método.
Comprobar el denominador en el programa principal en lugar de en __init__. Funciona hasta que alguien crea un Racional(1, 0) desde otro sitio. La validación tiene que estar donde nace el objeto.
Pruébalo tú
- Añade
ErrorDivisionCero(hija deErrorRacional) y un método__truediv__que la lance al dividir entre una fracción que vale 0. - Haz que
Racional(2, -4)guarde el signo en el numerador (-1/2) y comprueba la salida deprint(Racional(2, -4)).
Errores típicos en esta práctica
- El
tryfuera del bucle: el primer error saca delwhiley ya no se vuelve a pedir el dato. - Un
returndespués delexcept: si eltryfalló, la variable que devuelves no existe (UnboundLocalError). - Repetir lo que Python ya comprueba:
if b == 0: raise ZeroDivisionErrores código de más; Python lo lanza solo. - Un
except Exceptionen el lector de ficheros: una línea mala vacía toda la lectura sin decirte por qué. - Usar la variable del fichero en el
finallysin inicializarla: siopen()falla,fichero.close()revienta. - Capturar la madre antes que la hija: el
exceptconcreto no se ejecuta nunca y nadie te avisa.
Míralo paso a paso en Python Tutor
En Python Tutor (elige Python 3) se ve muy bien cómo viaja una excepción propia. Pega este programa, una versión mínima del programa 4:
class ErrorRacional(Exception):
pass
class ErrorDenominadorCero(ErrorRacional):
pass
def crear(num, den):
if den == 0:
raise ErrorDenominadorCero('denominador cero')
return (num, den)
for den in [4, 0]:
try:
r = crear(3, den)
print('Creado:', r)
except ErrorRacional as err:
print('Capturado:', type(err).__name__, err)
finally:
print('Siguiente')
Salida:
Creado: (3, 4) Siguiente Capturado: ErrorDenominadorCero denominador cero Siguiente
Qué observar:
- Al principio se crean las dos clases en el marco global: son objetos como cualquier otro.
- En la primera vuelta (
den = 4),creardevuelve la tupla(3, 4)y la flecha pasa deltryalfinally, sin tocar elexcept. - En la segunda vuelta, en el paso del
raise, el marco decreardesaparece y la flecha salta alexcept ErrorRacional, aunque la excepción seaErrorDenominadorCero. - La variable
rsigue valiendo(3, 4)de la vuelta anterior: la asignación de la segunda vuelta nunca terminó. - «Siguiente» se imprime en las dos vueltas: es el
finally.
Resumen
- Un bucle con un
trydentro y unreturncuando el dato vale es el patrón para pedir datos hasta que sean correctos. raiseen la función que detecta el problema yexcepten la que sabe qué hacer:calcularlanza, el bucle captura.- Deja que Python lance lo que ya sabe lanzar (
ZeroDivisionError,OverflowError) y usa su mensaje. - Dos niveles de
tryen los ficheros: el exterior para el fichero entero, el interior para cada línea. finally(owith) garantiza que el fichero se cierra; inicializa la variable aNonesi usasfinally.- Las excepciones propias en familia permiten capturar lo concreto o todo el grupo con un solo
except.
Checklist: ¿lo has practicado?
- ¿Sabes explicar por qué el
trydepedir_enterova dentro delwhile? - ¿Puedes decir, para cada línea de la calculadora, quién lanza la excepción, Python o nosotros?
- ¿Entiendes por qué «(fichero cerrado)» sale antes que el informe?
- ¿Sabrías añadir una hija nueva a la familia de
ErrorRacionaly capturarla? - ¿Has ejecutado los cuatro programas y comprobado las salidas?
- ¿Has intentado los «Pruébalo tú» sin mirar la teoría?
Si has respondido que sí a casi todo, estás listo para los ejercicios de excepciones en Python, donde ya no te doy el código hecho. ¡Nos vemos allí!
Siguiente paso: ejerciciosExcepciones en Python — ejercicios con un conversor, contraseñas con su familia de errores y una agenda que siempre se guarda (y el finally que cerró un fichero que no existía)¿Te ha servido este artículo? Así puedes apoyar SergioLearns →




Qué es un sistema operativo — teoría con tu portátil en una tarde de estudio, una llamada a write() y la CPU que nunca descansa (y los tres programas que no se ejecutaban a la vez)