Saltar a contenido

GESTIÓN DE ERRORES EN FICHEROS

Trabajar con ficheros implica interactuar con el sistema operativo y el disco. Muchas cosas pueden salir mal: el fichero no existe, no tenemos permisos de lectura, el disco está lleno, la ruta apunta a un directorio... Python lanza excepciones para comunicar estos errores.

Excepciones más comunes

FileNotFoundError

Se lanza cuando se intenta abrir un fichero en modo lectura ("r") y este no existe:

1
2
3
# Si notas.txt no existe, Python lanza FileNotFoundError
with open("notas.txt", "r", encoding="utf-8") as f:
    datos = f.read()
FileNotFoundError: [Errno 2] No such file or directory: 'notas.txt'

PermissionError

Se lanza cuando el proceso no tiene permisos suficientes para abrir el fichero (por ejemplo, intentar escribir en un fichero de solo lectura o en un directorio del sistema):

PermissionError: [Errno 13] Permission denied: '/etc/passwd'

IsADirectoryError

Se lanza cuando se intenta abrir una ruta que corresponde a un directorio, no a un fichero:

IsADirectoryError: [Errno 21] Is a directory: 'datos/'

FileExistsError

Se lanza cuando se intenta crear un fichero en modo exclusivo ("x") y este ya existe:

1
2
3
# Si backup.txt ya existe, Python lanza FileExistsError
with open("backup.txt", "x", encoding="utf-8") as f:
    f.write("datos iniciales")
FileExistsError: [Errno 17] File exists: 'backup.txt'

El modo "x" (exclusive creation) está diseñado precisamente para evitar sobreescrituras accidentales: falla si el fichero ya existe.

OSError e IOError

Son la clase base de la mayoría de errores relacionados con ficheros. IOError es un alias de OSError en Python 3. Si quieres capturar cualquier error de I/O sin especificarlo, puedes usar OSError.

Manejo con try / except

El bloque try / except permite capturar excepciones y reaccionar de forma controlada:

1
2
3
4
5
6
7
try:
    with open("notas.txt", "r", encoding="utf-8") as f:
        datos = f.read()
except FileNotFoundError:
    print("Error: el fichero 'notas.txt' no existe.")
except PermissionError:
    print("Error: no tienes permisos para leer este fichero.")

Capturar múltiples excepciones en un mismo bloque

Si queremos manejar varios tipos de error de la misma manera, podemos agruparlos en una tupla:

1
2
3
4
5
try:
    with open("notas.txt", "r", encoding="utf-8") as f:
        datos = f.read()
except (FileNotFoundError, PermissionError) as e:
    print(f"No se pudo abrir el fichero: {e}")

Acceder al mensaje del error con as

La cláusula as e asigna la excepción a una variable, lo que permite mostrar el mensaje de error original:

1
2
3
4
5
try:
    with open("configuracion.json", "r", encoding="utf-8") as f:
        datos = f.read()
except OSError as e:
    print(f"Error al acceder al fichero: {e}")

El orden de las excepciones importa

Cuando se encadenan varios except, Python los evalúa de arriba a abajo y ejecuta el primero que coincida. Por eso hay que colocar siempre las excepciones más específicas antes que las más generales.

Si pones OSError primero, nunca llegarás a FileNotFoundError aunque sea la excepción que realmente se lanzó, porque FileNotFoundError es una subclase de OSError y queda "absorbida" por el bloque más general:

1
2
3
4
5
6
7
8
# Incorrecto: FileNotFoundError nunca se captura por separado
try:
    with open("notas.txt", "r", encoding="utf-8") as f:
        datos = f.read()
except OSError:
    print("Error genérico de E/S.")   # captura todo, incluyendo FileNotFoundError
except FileNotFoundError:             # este bloque es inalcanzable
    print("El fichero no existe.")
# Correcto: primero el más específico, luego el más general
try:
    with open("notas.txt", "r", encoding="utf-8") as f:
        datos = f.read()
except FileNotFoundError:
    print("El fichero no existe.")
except PermissionError:
    print("Sin permisos de lectura.")
except OSError as e:
    print(f"Otro error de E/S: {e}")

La jerarquía de herencia de las excepciones de ficheros en Python es:

OSError
├── FileNotFoundError
├── PermissionError
├── FileExistsError
├── IsADirectoryError
└── ... (otras subclases)

Como regla general: de más específico a más general, igual que en una cadena if / elif.

El bloque finally

El bloque finally se ejecuta siempre, haya error o no. Se usa para limpiar recursos o ejecutar código que debe ocurrir en cualquier caso.

Cuando usamos with, el cierre del fichero ya está garantizado. El bloque finally es útil para "limpia" o realizar tareas que se ejecutan tanto si se completó la operación con éxito como si se lanzó alguna excepción. Por ejemplo:

  • Actualizar un contador.
  • Escribir un log de la operación
intentos = 0

try:
    with open("notas.txt", "r", encoding="utf-8") as f:
        datos = f.read()
        mensaje_log = "Fichero leído correctamente."
except FileNotFoundError:
    mensaje_log = "Error: fichero no encontrado."
finally:
    intentos += 1
    print(f"Intentos realizados: {intentos}")
    print(f"Log: " {mensaje_log})

Patrón completo: try / except / else / finally

Python permite usar else para ejecutar código solo cuando no se produce ninguna excepción:

try:
    with open("notas.txt", "r", encoding="utf-8") as f:
        datos = f.read()
except FileNotFoundError:
    print("El fichero no existe. Se iniciará con datos vacíos.")
    datos = ""
else:
    print(f"Fichero cargado: {len(datos)} caracteres.")
finally:
    print("Proceso de carga completado.")

Verificar antes de abrir

En ocasiones es más claro comprobar si el fichero existe antes de intentar abrirlo, usando pathlib.

A esta manera de gestionar los errores se le llama LBYL (Look Before You Leap).

Ejemplo:

from pathlib import Path

ruta = Path("notas.txt")

if ruta.exists():
    with open(ruta, "r", encoding="utf-8") as f:
        datos = f.read()
else:
    print("El fichero no existe todavía.")
    datos = ""

Aunque la verificación previa es válida, en Python se prefiere el estilo EAFP (Easier to Ask Forgiveness than Permission): intentar la operación y capturar el error si ocurre, en lugar de comprobar condiciones antes de actuar.

Ambos estilos son correctos; úsalos según lo que haga el código más legible.

Mala práctica: capturar Exception de forma genérica

# Incorrecto: captura cualquier error, incluyendo bugs del programa
try:
    with open("notas.txt", "r", encoding="utf-8") as f:
        datos = f.read()
except Exception:
    pass   # Silencia todos los errores, imposible depurar
# Correcto: captura solo los errores esperados y reacciona de forma útil
try:
    with open("notas.txt", "r", encoding="utf-8") as f:
        datos = f.read()
except FileNotFoundError:
    print("El fichero de notas no existe. Creando uno nuevo.")
    datos = ""
except PermissionError:
    print("Sin permisos de lectura. Comprueba la configuración.")
    raise

Tipos de verificaciones

Aparte de comprobar si una ruta existe, pathlib.Path permite hacer otras comprobaciones sobre rutas de archivo. Estas verificaciones ayudan a dar mensajes de error más precisos antes de abrir un recurso.

Las veremos con más detalle en el siguiente apartado sobre rutas.

Verificación Qué comprueba Cuándo usarla
Path.exists() Si la ruta existe en el sistema de ficheros Cuando quieres distinguir entre "no existe" y otros errores
Path.is_file() Si la ruta existe y es un fichero normal Antes de leer datos con open()
Path.is_dir() Si la ruta existe y es un directorio Cuando esperas una carpeta de trabajo
Path.suffix Extensión del fichero (.txt, .csv, .json...) Para validar formato esperado
Path.is_absolute() Si la ruta es absoluta Para evitar ambigüedad con rutas relativas

Ejemplo:

from pathlib import Path

ruta = Path("notas.txt")
if ruta.is_absolute():
    print("La ruta es absoluta.")
else:
    print("La ruta es relativa.")

if not ruta.exists():
    print("La ruta no existe.")
elif not ruta.is_file():
    print("La ruta existe, pero no es un fichero.")
elif ruta.suffix != ".txt":
    print("Formato no válido: se esperaba un .txt.")
else:
    with open(ruta, "r", encoding="utf-8") as f:
        datos = f.read()

Este enfoque LBYL mejora los mensajes al usuario, pero no elimina la necesidad de try/except: el estado del fichero puede cambiar entre la verificación y la apertura.

Aclaración

Un fichero se considera “normal” cuando es un archivo regular del sistema de ficheros:

  • contiene datos (texto/binario)
  • se puede abrir con open()
  • no es un directorio,
  • no es un dispositivo, socket, tubería, etc.