El bloque try de Python no es un simple try/catch como en otros lenguajes. Tiene cuatro cláusulas, y cada una existe por una razón precisa. Usarlas bien separa el código robusto del código que simplemente no falla todavía.
La estructura completa es: try contiene el código que puede lanzar; except captura excepciones específicas; else se ejecuta solo cuando try termina sin lanzar nada; y finally se ejecuta siempre, sin excepción, sin importar cómo termine el bloque. El orden de ejecución no es negociable y Python lo evalúa de arriba a abajo.
El detalle crítico está en except. Cuando escribes except ValueError as e, Python recorre los bloques except en orden y entra en el primero cuyo tipo sea igual o padre de la excepción lanzada. Esto tiene una consecuencia directa: el bloque más específico debe ir primero. Si pones except Exception antes de except ValueError, el ValueError nunca llega a su handler porque Exception lo captura antes. Python no te avisa de esto, simplemente entra en el bloque equivocado.
El antipatrón más peligroso es except: sin tipo, también llamado bare except. No solo captura Exception y sus subclases; captura todo, incluyendo BaseException y por tanto SystemExit, KeyboardInterrupt y GeneratorExit. Usarlo significa que Ctrl+C no puede matar tu proceso si está dentro de ese bloque, y que estás silenciando errores que no tienes intención de manejar. Usa siempre except Exception como mínimo si necesitas un catch-all, y mejor aún, sé explícito.
else es la cláusula más subestimada. Semánticamente dice: “esto solo tiene sentido si el try funcionó”. Meter ese código dentro del try es un error conceptual: si lanza una excepción, tus except podrían capturarla cuando en realidad no debería ser manejada por ellos. else da contexto al lector y al runtime.
finally garantiza ejecución incluso cuando hay un return o un raise en medio del bloque. Si el try hace return 42 y el finally hace return 0, el valor que sale es 0. Eso puede sorprenderte la primera vez que lo ves en producción.
import logging
from pathlib import Path
logger = logging.getLogger(__name__)
class ParseError(ValueError):
"""Raised when a record has an unexpected format."""
def parse_record(raw: str) -> dict:
parts = raw.strip().split(",")
if len(parts) != 3:
raise ParseError(f"Expected 3 fields, got {len(parts)}: {raw!r}")
name, age_str, score_str = parts
return {"name": name, "age": int(age_str), "score": float(score_str)}
def load_report(path: str) -> list[dict]:
records = []
file = None
try:
file = open(path, encoding="utf-8")
lines = file.readlines()
except FileNotFoundError:
# We can recover: return empty report instead of crashing.
logger.warning("Report file not found: %s", path)
return []
except PermissionError as e:
# Can't recover from this; re-raise with context.
raise RuntimeError(f"Cannot read report at {path}") from e
else:
# Runs only when open() and readlines() succeeded.
# ParseError here won't be silenced by the except blocks above.
for lineno, line in enumerate(lines, start=1):
if not line.strip():
continue
try:
records.append(parse_record(line))
except ParseError as e:
logger.error("Skipping line %d: %s", lineno, e)
except ValueError as e:
# int() or float() failed — more generic, so it comes after ParseError.
logger.error("Bad numeric value on line %d: %s", lineno, e)
finally:
# Runs no matter what: normal exit, return in try, or unhandled exception.
if file is not None:
file.close()
logger.debug("Closed %s", path)
return records
if __name__ == "__main__":
logging.basicConfig(level=logging.DEBUG)
report = load_report("data/sales.csv")
print(f"Loaded {len(records)} records") # type: ignore # intentional bug shown below
Vamos por partes. El bloque exterior tiene una estructura asimétrica intencionada: FileNotFoundError y PermissionError son errores de infraestructura que ocurren antes de tener datos útiles. El primero es recuperable (devolvemos lista vacía), el segundo no (re-lanzamos con raise ... from e, que preserva el traceback original como causa). Notar que raise X from Y es mejor que raise X a secas cuando hay contexto que perder.
El else externo es donde vive el procesamiento real. Si lo metiéramos dentro del try, un ParseError lanzado durante el bucle podría ser capturado por el except PermissionError as e si cometemos el error de cambiar el orden. En else, eso es imposible: esos except ya no están activos. Es una separación de responsabilidades dentro del mismo bloque.
El try interno por línea tiene except ParseError antes de except ValueError. ParseError hereda de ValueError, así que si pusieras el orden al revés, todo ParseError caería en el handler de ValueError y perderías la información de tipo. Python evalúa en orden de aparición, no por especificidad automática.
El finally cierra el fichero si llegó a abrirse. Fíjate en el guard if file is not None: si open() falla, file nunca se asignó y cerrar None daría AttributeError. Este es el patrón correcto cuando no usas context manager (aunque en producción preferirías with open(...) as file; aquí lo hacemos manual para ilustrar finally).
Errores que debes conocer
Error: usar except: sin tipo, que captura BaseException completo incluyendo KeyboardInterrupt y SystemExit, haciendo imposible interrumpir el proceso normalmente.
# ❌ Wrong
try:
risky_operation()
except:
print("something went wrong")
# ✅ Right
try:
risky_operation()
except Exception as e:
logger.error("Operation failed: %s", e)
except Exception excluye SystemExit, KeyboardInterrupt y GeneratorExit, que heredan de BaseException directamente y deben propagarse salvo que tengas una razón muy específica para interceptarlos.
Error: poner código de éxito dentro del try cuando pertenece al else, haciendo que sus excepciones sean capturadas por handlers que no deberían aplicar.
# ❌ Wrong
try:
data = fetch_data()
result = transform(data) # ValueError aquí caería en el except de abajo
except ConnectionError as e:
handle_network_error(e)
# ✅ Right
try:
data = fetch_data()
except ConnectionError as e:
handle_network_error(e)
else:
result = transform(data) # ValueError aquí se propaga sin interferencia
else acota el try al código que realmente puede fallar de la forma que estás manejando, y deja que el resto de excepciones se propaguen con fidelidad.
Error: devolver un valor en finally sin darse cuenta de que suprime cualquier return o raise que venía del try o del except.
# ❌ Wrong — el raise de except nunca llega al caller
def load():
try:
return fetch()
except TimeoutError:
raise RuntimeError("Timeout")
finally:
return [] # silencia el raise anterior; el caller recibe [] sin saber nada
# ✅ Right
def load():
try:
return fetch()
except TimeoutError:
raise RuntimeError("Timeout")
finally:
cleanup() # solo efectos secundarios, nunca return ni raise aquí
finally debe limitarse a liberar recursos; un return o raise dentro de él anula cualquier flujo de control previo y produce comportamientos que son muy difíciles de debuguear en producción.
N° 76