Skip to content
This page has been auto-translated and may contain errors.View in English

Archivos y excepciones

La mayoría de los programas que hacen trabajo real interactúan con el sistema de archivos: leen una configuración, escriben resultados, cargan datos. Y cuando algo sale mal, Python lanza una excepción, una señal de que ocurrió algo inesperado. Este capítulo cubre ambos temas: cómo introducir y extraer datos de archivos, y cómo escribir código que maneje los errores con elegancia en lugar de fallar abruptamente.

La E/S de archivos y el manejo de excepciones son los dos mecanismos que hacen robustos a los programas. open() da acceso al sistema de archivos; with garantiza que la limpieza ocurra incluso ante errores. try/except captura tipos específicos de excepciones, lo que te permite recuperarte con elegancia en lugar de fallar. Juntos forman la base de cualquier script que se ejecute sin supervisión.

Las operaciones de archivos en Python pasan por la abstracción de descriptores de archivo del sistema operativo. open() devuelve un objeto archivo cuyo tipo depende del modo y la configuración de buffering. Los administradores de contexto garantizan la liberación de recursos a través de __enter__ y __exit__. El manejo de excepciones utiliza desenrollado estructurado: cuando una excepción se propaga, Python recorre la pila de llamadas buscando cláusulas except que coincidan, ejecutando los bloques finally en el camino sin importar el resultado.

Apertura de archivos

open() abre un archivo y devuelve un objeto desde el que puedes leer o en el que puedes escribir. Le indicas la ruta y qué quieres hacer con el archivo (leer, escribir o agregar). Siempre cierra un archivo cuando termines; la sentencia with lo hace automáticamente.

open(path, mode) devuelve un objeto archivo. La cadena de modo controla el acceso: "r" para lectura, "w" para escritura (truncando primero), "a" para agregar. Añadir "b" activa el modo binario. La codificación por defecto en modo texto es la del locale del sistema; especifica encoding="utf-8" explícitamente para garantizar portabilidad.

open() invoca al SO para asignar un descriptor de archivo y devuelve un wrapper de E/S con búfer. El modo texto envuelve con TextIOWrapper, que maneja la codificación y los saltos de línea universales. El modo binario devuelve un BufferedReader o BufferedWriter. El parámetro encoding es crítico para la portabilidad: el valor por defecto (locale.getpreferredencoding()) varía según la plataforma. Especifica siempre encoding="utf-8" para archivos de texto a menos que haya una razón específica para no hacerlo.

python
f = open("data.txt", "r")    # "r" = lectura
content = f.read()
f.close()

El "r" es el modo:

ModoSignificado
"r"Lectura. El archivo debe existir. Modo por defecto.
"w"Escritura. Crea o sobrescribe el archivo.
"a"Agregar. Añade al final sin borrar.
"x"Crear. Falla si el archivo ya existe.
"r+"Lectura y escritura.
"b"Binario. Se agrega a cualquier modo: "rb", "wb".

Llama siempre a .close() cuando termines. Olvidarlo deja el archivo bloqueado y puede causar corrupción de datos. La forma confiable de manejar esto es la sentencia with.

La sentencia with

with open(...) se encarga del archivo por ti, cerrándolo automáticamente cuando el bloque indentado termina, incluso si ocurre un error. Usa siempre with open(...) en lugar de open()/close() manual. Es más seguro y es el estándar.

with es la sintaxis de administradores de contexto de Python. Llama a __enter__ al inicio y a __exit__ al final, incluso si se lanza una excepción. Para los archivos, __exit__ cierra el descriptor del archivo. Esto garantiza la liberación sin requerir envolver cada acceso a archivos en un try/finally.

with expr as name llama a expr.__enter__() y vincula el resultado a name. Al salir (normal o por excepción), se llama a expr.__exit__(exc_type, exc_val, tb). Si __exit__ devuelve un valor verdadero, la excepción es suprimida. Los objetos archivo implementan este protocolo: __exit__ llama a self.close(). Los administradores de contexto pueden apilarse: with open(a) as f, open(b) as g: es idiomático para trabajar con múltiples archivos.

python
with open("data.txt", "r") as f:
    content = f.read()

# f se cierra aquí, garantizado

¿Qué hace with?

with es la sintaxis de administradores de contexto de Python. Llama por ti al código de configuración y liberación; en este caso, abrir y cerrar el archivo de manera confiable. No necesitas saber cómo funciona internamente. Solo úsalo con open().

Lectura de archivos

Tres métodos para leer. .read() carga el archivo completo como una sola cadena. .readline() lee una línea. Iterar directamente sobre el objeto archivo lee línea por línea, que es la forma más eficiente para archivos grandes, ya que no carga todo en memoria de una sola vez.

.read() carga el archivo completo en memoria. .readline() lee una línea incluyendo el carácter de nueva línea. .readlines() devuelve una lista con todas las líneas. Iterar directamente sobre el objeto archivo es el patrón más eficiente en memoria para archivos grandes: lee una línea a la vez del búfer sin retener el contenido completo.

.read() lee hasta EOF, devolviendo el contenido como cadena (o bytes en modo binario). .readline() lee hasta \n o EOF. Iterar el objeto archivo llama a __iter__, que llama repetidamente a readline(): memoria O(1), procesamiento línea por línea. .readlines() es equivalente a list(file) pero materializa todas las líneas de inmediato. Para archivos muy grandes, se prefiere el patrón de iterador sobre .read().

python
with open("data.txt", "r") as f:
    content = f.read()          # archivo completo como una sola cadena

with open("data.txt", "r") as f:
    first_line = f.readline()   # una línea a la vez

with open("data.txt", "r") as f:
    lines = f.readlines()       # lista de líneas, cada una termina en "\n"

Para archivos grandes, leer línea por línea es más eficiente que cargar todo de una vez:

python
with open("big_file.txt", "r") as f:
    for line in f:              # iterar el archivo directamente, eficiente en memoria
        print(line.strip())     # strip() elimina el salto de línea final

Iterar directamente sobre el objeto archivo (for line in f) es la forma más eficiente e idiomática de leer un archivo grande.

Escritura de archivos

El modo "w" sobrescribe el archivo por completo si existe. El modo "a" añade al final. .write() no agrega un salto de línea automáticamente; incluye "\n" explícitamente al final de cada línea. Para escribir varias líneas a la vez, únelas con "\n".join().

.write(s) escribe una cadena y devuelve el número de caracteres escritos. No agrega un salto de línea. .writelines(iterable) escribe cada cadena del iterable sin agregar separadores. "w" trunca al abrir; "a" se posiciona al final del archivo. Para portabilidad, usa "\n" o os.linesep en lugar de codificar de forma rígida los terminadores de línea específicos de la plataforma.

.write() escribe en el búfer; los datos pueden no llegar al disco hasta que se llame a .flush() o .close(). El modo "w" llama a truncate(0) al abrir, destruyendo cualquier contenido previo. El modo "a" se posiciona al final del archivo antes de cada escritura, lo que lo hace seguro para escritores concurrentes que agregan (en la mayoría de los SO). f.writelines() llama a f.write() por cada elemento: sin asignación extra de memoria pero tampoco con separadores agregados.

python
with open("output.txt", "w") as f:
    f.write("Hola, mundo\n")

with open("output.txt", "a") as f:
    f.write("Otra línea\n")

"w" sobrescribe el archivo por completo si existe. "a" añade al final.

f.write() no agrega un salto de línea automáticamente, así que incluye "\n" explícitamente. Para escribir varias líneas a la vez:

python
lines = ["Línea uno", "Línea dos", "Línea tres"]

with open("output.txt", "w") as f:
    f.write("\n".join(lines) + "\n")

Excepciones

Cuando Python se encuentra con un problema que no puede manejar, lanza una excepción: un error que describe qué salió mal y dónde. Si no la manejas, tu programa falla e imprime un traceback. La siguiente tabla muestra las excepciones más comunes con las que te encontrarás.

Las excepciones son objetos que heredan de BaseException. Todas las excepciones orientadas al usuario heredan de Exception. Cuando se lanza una, Python desenrolla la pila de llamadas buscando una cláusula except que coincida. El traceback muestra la cadena completa de llamadas desde el punto de la excepción hasta el punto de entrada.

Los objetos de excepción llevan tipo, mensaje y un atributo __traceback__ que apunta al objeto traceback. raise crea una nueva excepción; raise sin argumento re-lanza la excepción actual preservando el traceback original. El encadenamiento de excepciones (raise B from A) vincula dos excepciones y se muestra en el traceback. BaseException es la raíz; KeyboardInterrupt y SystemExit heredan directamente de ella, no de Exception, razón por la cual un except Exception desnudo no las captura.

Excepciones comunes con las que te encontrarás:

ExcepciónCuándo ocurre
FileNotFoundErroropen() no puede encontrar el archivo
ValueErrorLa función recibe un valor del tipo correcto pero contenido incorrecto, p. ej. int("abc")
TypeErrorTipo totalmente incorrecto, p. ej. "hello" + 5
KeyErrorLa clave del diccionario no existe
IndexErrorÍndice de lista fuera de rango
ZeroDivisionErrorDivisión por cero
AttributeErrorEl objeto no tiene ese atributo o método

try / except

Envuelve en un bloque try el código que puede fallar. Si ocurre una excepción, el bloque except correspondiente la maneja en lugar de fallar. Sé específico sobre qué excepción capturas: capturar todo con un except: desnudo oculta bugs reales.

try/except intercepta tipos específicos de excepciones. Especifica el tipo para evitar tragarte silenciosamente errores no relacionados. Puedes vincular la excepción a un nombre con as e para inspeccionar el mensaje. Múltiples cláusulas except manejan diferentes tipos de excepciones; Python coincide con la primera cláusula compatible.

except ExceptionType as e coincide si isinstance(raised_exception, ExceptionType) es verdadero. Esto significa que capturar Exception también captura todas las subclases. Python coincide con la primera cláusula except cuyo tipo coincida; las cláusulas siguientes se omiten. except (A, B) as e captura cualquiera de los dos tipos. Un except: desnudo captura todo incluyendo KeyboardInterrupt y SystemExit, lo cual casi nunca tiene sentido.

python
try:
    value = int("abc")
except ValueError:
    print("Eso no es un número válido")

Sé específico sobre qué excepción capturas. Capturar todas las excepciones con un except: desnudo oculta bugs:

python
# mal, captura todo incluyendo errores de programación
try:
    result = do_something()
except:
    pass

# bien, solo captura lo que esperas y puedes manejar realmente
try:
    result = do_something()
except FileNotFoundError:
    print("Archivo no encontrado")

Capturar múltiples excepciones

Puedes manejar diferentes tipos de error en bloques except separados, o capturar varios tipos en un solo bloque usando una tupla. La parte as e te da acceso al mensaje del error.

Las múltiples cláusulas except se evalúan de arriba hacia abajo; gana la primera coincidencia. Capturar varios tipos en una tupla captura cualquiera de ellos. Usar as e vincula la instancia de la excepción, dando acceso al mensaje, tipo y traceback. Capturar primero el tipo más específico y los más generales después evita que se solapen.

except (A, B) as e es azúcar sintáctico para una sola cláusula que captura ambos. El vínculo as e se borra después de que sale el bloque except (para evitar ciclos de referencia con el traceback). Para re-lanzar con contexto adicional, usa raise ValueError("context") from e, que establece __cause__ y muestra ambas excepciones en el traceback.

python
try:
    data = int(user_input)
    result = 100 / data
except ValueError:
    print("No es un número")
except ZeroDivisionError:
    print("No se puede dividir por cero")

O capturar múltiples en una tupla:

python
except (ValueError, ZeroDivisionError) as e:
    print(f"Error de entrada: {e}")

as e vincula el objeto de la excepción a un nombre para que puedas inspeccionar el mensaje.

else y finally

else se ejecuta solo si no ocurrió ninguna excepción. finally siempre se ejecuta, haya habido o no una excepción. finally es útil para tareas de limpieza que deben ocurrir sin importar qué.

else separa el código de la "ruta normal" del cuerpo del try, dejando claro que el código en else solo se ejecuta cuando no se lanzó ninguna excepción. finally es la garantía de limpieza: se ejecuta incluso si se lanzó, se capturó o se re-lanzó una excepción. Incluso se ejecuta si se encuentra return o break.

else se ejecuta si el bloque try se completó sin lanzar nada. finally se ejecuta incondicionalmente, incluso después de return, break, continue o una excepción no manejada. Si tanto finally como el código que llama tienen sentencias return, el return de finally tiene precedencia. finally con handles de archivos o conexiones de base de datos es una red de seguridad secundaria cuando with no está disponible.

python
try:
    with open("data.txt") as f:
        content = f.read()
except FileNotFoundError:
    print("Archivo no encontrado, usando valores por defecto")
    content = ""
else:
    print("Archivo cargado con éxito")
finally:
    print("Se terminó el intento de cargar el archivo")   # siempre se ejecuta

finally es más útil para tareas de limpieza (cerrar conexiones, liberar bloqueos) incluso cuando ya estás usando with para los archivos.

raise

Puedes lanzar excepciones tú mismo con raise. Así es como haces que tus funciones señalen problemas claramente a quienes las llaman, en lugar de devolver silenciosamente un valor incorrecto.

raise ExceptionType("mensaje") crea y lanza una excepción. raise sin argumento re-lanza la excepción actual dentro de un bloque except. Lanzar excepciones desde tus funciones hace explícitas sus condiciones de error, para que quienes las llamen puedan manejarlas específicamente.

raise expr evalúa expr para obtener una instancia de excepción y establece __traceback__. raise solo re-lanza la excepción actual sin modificar el traceback. raise B from A establece B.__cause__ = A (encadenamiento explícito); raise B from None suprime la visualización del contexto. Después de capturar y registrar, raise es la forma limpia de propagar preservando el traceback original.

python
def divide(a, b):
    if b == 0:
        raise ValueError("Cannot divide by zero")
    return a / b

Esto hace que tus funciones sean explícitas sobre lo que esperan y señalen los problemas claramente a quienes las llaman.

Clases de excepción personalizadas

Para programas más grandes, puedes definir tus propios tipos de excepción heredando de Exception. Esto permite que quienes llaman capturen tus errores específicos por separado de otros tipos de errores.

Las excepciones personalizadas crean una jerarquía que quienes llaman pueden capturar al nivel correcto de especificidad. Normalmente, subclasear Exception es todo lo que necesitas. Para familias de excepciones, crea una clase de excepción base y subclaséala para modos de error específicos; quienes llamen pueden entonces capturar el tipo base para manejar todas las variantes.

Las excepciones personalizadas deben subclasear Exception (no BaseException). Agregar __init__ con campos específicos del dominio permite que quienes llaman inspeccionen la excepción programáticamente, no solo leer la cadena del mensaje. Las jerarquías de excepciones permiten a quienes llaman elegir su nivel de especificidad: except PaymentError captura todos los errores relacionados con pagos; except InsufficientFundsError captura solo ese caso específico.

python
class InsufficientFundsError(Exception):
    pass

class BankAccount:
    def __init__(self, balance):
        self.balance = balance

    def withdraw(self, amount):
        if amount > self.balance:
            raise InsufficientFundsError(
                f"Cannot withdraw {amount}, balance is {self.balance}"
            )
        self.balance -= amount
python
try:
    account.withdraw(1000)
except InsufficientFundsError as e:
    print(f"Transaction declined: {e}")

JSON

JSON es el formato que todo el mundo habla: APIs, archivos de configuración, exportaciones de datos. El módulo json de Python lo maneja directamente. json.load() lee JSON desde un archivo a un dict o list de Python. json.dump() escribe un dict o list de Python a un archivo como JSON.

json.load() y json.dump() trabajan con objetos archivo. json.loads() y json.dumps() trabajan con cadenas. El parámetro indent= en dump/dumps hace que la salida sea legible para humanos. json.JSONDecodeError (una subclase de ValueError) se lanza ante JSON inválido.

json.load() es un parser por flujos: lee del objeto archivo de forma incremental. json.dumps() devuelve una cadena; json.dump() escribe en un objeto similar a archivo que soporte .write(). El parámetro default= de dump/dumps maneja tipos que no son serializables a JSON; recibe el objeto no serializable y debe devolver algo serializable. object_hook en load/loads intercepta cada dict de objeto parseado, habilitando deserialización personalizada.

Leer JSON desde un archivo:

python
import json

with open("config.json", "r") as f:
    config = json.load(f)    # parsea JSON a un dict/list de Python

print(config["setting"])

Escribir JSON a un archivo:

python
import json

data = {"name": "Sofía", "score": 87, "active": True}

with open("output.json", "w") as f:
    json.dump(data, f, indent=2)    # indent= lo hace legible para humanos

Mapeo de tipos JSON a Python:

JSONPython
object {}dict
array []list
string ""str
numberint o float
true / falseTrue / False
nullNone

Para convertir entre cadenas JSON y objetos Python sin tocar un archivo:

python
import json

# cadena a Python
data = json.loads('{"name": "Sofía", "score": 87}')

# Python a cadena
text = json.dumps({"name": "Sofía", "score": 87}, indent=2)

json.load() lee desde un objeto archivo. json.loads() (con una "s") lee desde una cadena.

En la práctica

Un patrón de guardar/cargar para un juego simple: escribir el estado a JSON, cargarlo de nuevo en la siguiente ejecución y recurrir a valores por defecto si aún no existe un archivo de guardado:

python
import json

SAVE_FILE = "save_game.json"

def save_game(player_data: dict) -> None:
    with open(SAVE_FILE, "w") as f:
        json.dump(player_data, f, indent=2)
    print("Juego guardado.")

def load_game() -> dict:
    try:
        with open(SAVE_FILE, "r") as f:
            return json.load(f)
    except FileNotFoundError:
        print("No se encontró archivo de guardado, empezando desde cero.")
        return {"name": "Player", "score": 0, "level": 1}

state = load_game()
state["score"] += 50
save_game(state)

Cargar un archivo de configuración y guardar resultados, con manejo específico de excepciones para cada modo de fallo:

python
import json

def load_config(path: str) -> dict:
    try:
        with open(path, "r") as f:
            return json.load(f)
    except FileNotFoundError:
        raise FileNotFoundError(f"Config file not found: {path}")
    except json.JSONDecodeError as e:
        raise ValueError(f"Invalid JSON in {path}: {e}")

def save_results(results: list[dict], path: str) -> None:
    with open(path, "w") as f:
        json.dump(results, f, indent=2)
    print(f"Saved {len(results)} result(s) to {path}")

config  = load_config("experiment.json")
results = [{"epoch": 1, "loss": 0.82}, {"epoch": 2, "loss": 0.61}]
save_results(results, "results.json")

Un escritor de registros estructurado que añade entradas con marca de tiempo a un archivo, con un manejador de nivel superior que captura y registra fallos inesperados:

python
import json
from datetime import datetime

LOG_FILE = "run.log"

def log(level: str, message: str) -> None:
    ts    = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    entry = f"[{ts}] [{level.upper():7}] {message}\n"
    with open(LOG_FILE, "a") as f:
        f.write(entry)

def process(config_path: str) -> None:
    log("info", f"Starting job, config: {config_path}")
    try:
        with open(config_path) as f:
            config = json.load(f)
        log("info", f"Loaded config: {config}")
    except FileNotFoundError:
        log("error", f"Config not found: {config_path}")
        raise
    except json.JSONDecodeError as e:
        log("error", f"Bad JSON in config: {e}")
        raise

try:
    process("config.json")
except Exception as e:
    log("critical", f"Job failed: {e}")

Re-lanzar después de registrar preserva el traceback original para quien llama. El except Exception de nivel superior captura cualquier cosa que se haya colado, lo registra como crítico y permite que el proceso termine limpiamente.