Archivos y manejo de excepciones
"El programa que asume que todo siempre va a funcionar también asume que el universo coopera. El universo, históricamente, no coopera."
Qué vas a aprender en este capítulo
Hasta ahora tus programas perdían todo al cerrarse. En este capítulo aprendés a persistir datos en archivos: leer, escribir, manejar formatos comunes (JSON, CSV). También aprendés a manejar errores con try/except: porque archivos faltantes, permisos denegados, datos corruptos y disco lleno son la norma, no la excepción. Al final vas a tener todas las piezas para que tu programa hable con el mundo y no se caiga al primer problema.
4.1 La idea: la memoria que persiste
💡 Intuición
Imaginate que estás escribiendo en una hoja, y cada vez que cerrás los ojos, la hoja se borra. Esa es la memoria de tu programa: existe mientras corre. Al cerrarse, todo desaparece.
Para que un programa recuerde algo entre ejecuciones, hay que escribir a un archivo (o base de datos, o servicio de red — más adelante). El SO se encarga de que esos bytes sigan en el disco mañana.
Un archivo es un cubo de bytes con un nombre, persistente. Tu programa puede abrirlo, leer, escribir y cerrarlo. Las syscalls correspondientes son
open,read,write,close— pero Python las envuelve en una API limpia.
Y hablando de errores: lo que distingue programas profesionales de programas escolares es cómo manejan lo inesperado. ¿Y si el archivo no existe? ¿Y si el JSON está malformado? ¿Y si se llenó el disco? Un programa serio anticipa y maneja. Un programa de juguete explota.
4.2 Abrir y cerrar archivos
📐 Fundamento
Forma básica:
f = open("archivo.txt", "r")
contenido = f.read()
f.close()
open() devuelve un objeto archivo. f.read() lee TODO el contenido. f.close() libera el recurso.
Modos:
| Modo | Para qué |
|---|---|
"r" |
Lectura (default). Falla si no existe. |
"w" |
Escritura. Trunca (borra contenido) si existe; crea si no. |
"a" |
Append. Escribe al final. |
"x" |
Crear. Falla si existe. |
"r+" |
Lectura/escritura, no trunca. |
"b" |
Modo binario (combiná: "rb", "wb"). |
"t" |
Modo texto (default). |
encoding="utf-8" es buena costumbre — evita sorpresas con caracteres especiales:
f = open("archivo.txt", "r", encoding="utf-8")
El problema del olvido
Si tu programa crashea entre open y close, el archivo queda abierto y posiblemente sin escribirse. Eso es bug.
Solución idiomática: with (context manager).
with open("archivo.txt", "r") as f:
contenido = f.read()
# acá afuera, f ya está cerrado, GARANTIZADO
with garantiza que f.close() se llame al salir del bloque, incluso si hay excepción. Es la forma moderna y siempre la preferida.
4.3 Leer archivos
📐 Fundamento
Tres formas principales de leer:
1. Todo de una vez
with open("archivo.txt") as f:
texto = f.read() # devuelve un string completo
OK para archivos chicos. Mal para archivos grandes (carga todo a RAM).
2. Línea por línea
with open("archivo.txt") as f:
for linea in f:
print(linea.rstrip()) # rstrip quita el \n
Eficiente. Procesa una línea, descarta, sigue. Funciona para archivos enormes.
3. readlines (devuelve lista de líneas)
with open("archivo.txt") as f:
lineas = f.readlines() # ["linea1\n", "linea2\n", ...]
OK si necesités acceso aleatorio por índice.
Path
Aceptar paths absolutos o relativos. Buenas prácticas modernas usan pathlib:
from pathlib import Path
p = Path("datos") / "archivo.txt"
texto = p.read_text(encoding="utf-8")
Path es portable (Windows/Linux/Mac), tiene operadores cómodos (/ para concatenar), y métodos como .read_text(), .write_text(), .exists(), .glob().
4.4 Escribir archivos
📐 Fundamento
with open("salida.txt", "w") as f:
f.write("Hola\n")
f.write("Mundo\n")
f.write no agrega \n automáticamente (a diferencia de print).
Escribir varias líneas con print redirigido:
with open("salida.txt", "w") as f:
print("Hola", file=f)
print("Mundo", file=f)
Más cómodo si querés print normal.
Append (agregar sin borrar):
with open("log.txt", "a") as f:
f.write(f"{fecha} - usuario inició sesión\n")
Modo binario. Para imágenes, ZIPs, archivos no-texto:
with open("foto.jpg", "rb") as f:
bytes_imagen = f.read()
with open("copia.jpg", "wb") as f:
f.write(bytes_imagen)
Modo binario te da bytes, no str. Para conversión: bytes.decode("utf-8") y str.encode("utf-8").
4.5 Formatos comunes: CSV
🛠️ En la práctica
CSV (Comma-Separated Values) es el formato más común para datos tabulares.
Leer:
import csv
with open("estudiantes.csv", encoding="utf-8") as f:
reader = csv.reader(f)
encabezado = next(reader) # primera fila
for fila in reader:
print(fila) # ['Ana', '19', '7.5']
Mejor con DictReader:
with open("estudiantes.csv", encoding="utf-8") as f:
for fila in csv.DictReader(f):
print(fila["nombre"], fila["edad"])
Lee el encabezado automáticamente y devuelve cada fila como dict.
Escribir:
import csv
with open("salida.csv", "w", newline="", encoding="utf-8") as f:
writer = csv.writer(f)
writer.writerow(["nombre", "edad", "promedio"])
writer.writerow(["Ana", 19, 7.5])
writer.writerow(["Beto", 20, 8.0])
newline="" es crucial en Windows — sin él los archivos quedan con dobles saltos de línea.
Excel y separadores. Excel en español usa ; como separador, no ,. Cargar CSV "español":
csv.reader(f, delimiter=";")
4.6 Formatos comunes: JSON
📐 Fundamento
JSON (JavaScript Object Notation) es el formato estándar para datos estructurados, web APIs, configuración.
Leer:
import json
with open("datos.json", encoding="utf-8") as f:
datos = json.load(f)
print(datos) # un dict o lista, según el archivo
Escribir:
datos = {
"estudiantes": ["ana", "beto"],
"promedios": {"ana": 8.5, "beto": 7.0},
}
with open("datos.json", "w", encoding="utf-8") as f:
json.dump(datos, f, indent=2, ensure_ascii=False)
indent=2 formatea bonito. ensure_ascii=False permite tildes y eñes.
Conversión Python → JSON:
| Python | JSON |
|---|---|
dict |
object |
list, tuple |
array |
str |
string |
int, float |
number |
True, False |
true, false |
None |
null |
De string (no archivo):
texto = '{"nombre": "ana", "edad": 19}'
datos = json.loads(texto) # carga desde string
texto = json.dumps(datos, indent=2) # serializa a string
JSON no soporta: datetime, set, función, NaN. Para esos hay extensiones o serialización custom.
4.7 Excepciones — cuando todo falla
📐 Fundamento
Una excepción es un evento anormal: archivo no existe, división por cero, índice fuera de rango, RAM agotada. Si no la manejás, tu programa muere (con un traceback).
Sintaxis:
try:
f = open("inexistente.txt")
contenido = f.read()
except FileNotFoundError:
print("Archivo no existe")
try ejecuta el código riesgoso. Si dentro se levanta una excepción del tipo capturado, salta al except correspondiente.
Capturar varios tipos:
try:
n = int(input("Número: "))
r = 100 / n
except ValueError:
print("No es un número")
except ZeroDivisionError:
print("No se puede dividir por 0")
Capturar todos (con cuidado):
try:
# algo
except Exception as e:
print(f"Error: {e}")
Exception captura casi todas (no KeyboardInterrupt ni SystemExit, que son señales). as e te da el objeto para inspección.
else y finally
Forma completa:
try:
# código riesgoso
except SomeError:
# manejo
else:
# ejecuta si NO hubo excepción
finally:
# SIEMPRE ejecuta (con o sin excepción)
finally es para liberar recursos que tomaste. Es lo que hace with por dentro:
f = open("archivo.txt")
try:
contenido = f.read()
finally:
f.close() # cierra incluso si hay error
else se usa menos. Va donde el código exitoso "principal" sin estorbar el try.
Jerarquía
Las excepciones forman jerarquía. FileNotFoundError hereda de OSError que hereda de Exception que hereda de BaseException.
BaseException
└── Exception
├── ValueError
├── TypeError
├── KeyError
├── IndexError
├── ArithmeticError
│ └── ZeroDivisionError
├── OSError
│ ├── FileNotFoundError
│ ├── PermissionError
│ └── ...
└── ...
Capturar la superclase captura todas las subclases. Por eso except Exception atrapa casi todo.
Lo más específico arriba:
try:
...
except FileNotFoundError:
... # más específico
except OSError:
... # más general
except Exception:
... # último recurso
4.8 Levantar excepciones
📐 Fundamento
Tu propio código puede levantar errores con raise:
def dividir(a, b):
if b == 0:
raise ValueError("Divisor no puede ser cero")
return a / b
Definir tus propias:
class StockInsuficiente(Exception):
"""Stock no alcanza para el pedido."""
def __init__(self, producto, pedido, disponible):
self.producto = producto
self.pedido = pedido
self.disponible = disponible
super().__init__(f"{producto}: pedidas {pedido}, disponibles {disponible}")
def vender(producto, cantidad, inventario):
if inventario.get(producto, 0) < cantidad:
raise StockInsuficiente(producto, cantidad, inventario.get(producto, 0))
inventario[producto] -= cantidad
try:
vender("queso", 100, {"queso": 5})
except StockInsuficiente as e:
print(f"Lo siento: {e}")
print(f" Disponible: {e.disponible}")
Por qué crear excepciones propias:
- Diferenciar errores de tu dominio del resto. Un
ValueErrorgenérico es ambiguo; unStockInsuficientees claro. - Llevar info estructurada. Atributos del objeto excepción.
- Manejo selectivo. Distintos handlers para distintos errores.
4.9 Patrón "EAFP" vs "LBYL"
📐 Fundamento
Dos estilos para manejar errores:
LBYL (Look Before You Leap) — chequear antes:
import os
if os.path.exists("archivo.txt"):
with open("archivo.txt") as f:
contenido = f.read()
else:
contenido = ""
EAFP (Easier to Ask Forgiveness than Permission) — intentar y atrapar:
try:
with open("archivo.txt") as f:
contenido = f.read()
except FileNotFoundError:
contenido = ""
Python prefiere EAFP por:
- Race conditions. Entre el check y el uso, el archivo puede aparecer/desaparecer. EAFP no tiene ese problema.
- Más rápido en el caso feliz (la verificación tiene costo siempre, aunque rara vez falle).
- Más legible — el código del happy path no se contamina con chequeos.
Cuándo LBYL: chequeos baratos antes de operaciones caras. Por ejemplo, validar input del usuario antes de mandarlo a la base de datos.
4.10 Proyecto: persistir el menú
🏗️ Avance del proyecto
Hasta ahora la pupusería pierde todo cuando cierra el programa. Vamos a persistir el menú y los pedidos en archivos JSON.
# pupuseria_v4.py - persistencia con JSON
import json
from pathlib import Path
from datetime import datetime
ARCHIVO_MENU = Path("menu.json")
ARCHIVO_PEDIDOS = Path("pedidos.json")
IVA = 0.13
# ---------------------- excepciones propias ----------------------
class CodigoInvalido(Exception):
pass
class StockInsuficiente(Exception):
def __init__(self, codigo, pedido, disponible):
super().__init__(f"{codigo}: pedidas {pedido}, hay {disponible}")
self.codigo, self.pedido, self.disponible = codigo, pedido, disponible
# ---------------------- carga y guardado ----------------------
def cargar_menu():
"""Carga el menú desde archivo. Si no existe, devuelve uno default."""
try:
return json.loads(ARCHIVO_MENU.read_text(encoding="utf-8"))
except FileNotFoundError:
# Primera vez — crear default
default = {
"Q": {"nombre": "Pupusa de queso", "precio": 0.50, "stock": 100},
"R": {"nombre": "Pupusa revuelta", "precio": 0.60, "stock": 80},
"F": {"nombre": "Pupusa de frijol", "precio": 0.55, "stock": 60},
}
guardar_menu(default)
return default
except json.JSONDecodeError as e:
raise RuntimeError(f"menu.json está corrupto: {e}")
def guardar_menu(menu):
ARCHIVO_MENU.write_text(
json.dumps(menu, indent=2, ensure_ascii=False),
encoding="utf-8",
)
def registrar_pedido(pedido, total):
"""Append-only del pedido a pedidos.json."""
historial = []
try:
historial = json.loads(ARCHIVO_PEDIDOS.read_text(encoding="utf-8"))
except FileNotFoundError:
pass # archivo nuevo
except json.JSONDecodeError:
pass # corrupto, ignorar (en producción habría que loguear)
historial.append({
"fecha": datetime.now().isoformat(timespec="seconds"),
"items": pedido,
"total": round(total, 2),
})
ARCHIVO_PEDIDOS.write_text(
json.dumps(historial, indent=2, ensure_ascii=False),
encoding="utf-8",
)
# ---------------------- lógica ----------------------
def vender(menu, codigo, cantidad):
if codigo not in menu:
raise CodigoInvalido(codigo)
p = menu[codigo]
if p["stock"] < cantidad:
raise StockInsuficiente(codigo, cantidad, p["stock"])
p["stock"] -= cantidad
return cantidad * p["precio"]
def imprimir_menu(menu):
print("\n=== Menú ===")
for codigo, p in menu.items():
print(f" [{codigo:3}] {p['nombre']:25} ${p['precio']:.2f} stock: {p['stock']}")
print()
def main():
menu = cargar_menu()
imprimir_menu(menu)
pedido = {}
subtotal = 0
while True:
s = input("Código (o 'fin'): ").strip().upper()
if s == "FIN":
break
cant_s = input(" Cantidad: ").strip()
try:
cantidad = int(cant_s)
importe = vender(menu, s, cantidad)
except CodigoInvalido as e:
print(f" ✗ Código '{e.args[0]}' no existe")
continue
except StockInsuficiente as e:
print(f" ✗ Stock insuficiente: {e}")
continue
except ValueError:
print(" ✗ Cantidad inválida")
continue
pedido[s] = pedido.get(s, 0) + cantidad
subtotal += importe
print(f" ✓ Acumulado: ${subtotal:.2f}")
if not pedido:
print("Sin pedido.")
return
iva = subtotal * IVA
total = subtotal + iva
print(f"\n=== TOTAL ===")
print(f" Subtotal: ${subtotal:.2f}")
print(f" IVA: ${iva:.2f}")
print(f" Total: ${total:.2f}")
try:
guardar_menu(menu)
registrar_pedido(pedido, total)
print("✓ Guardado")
except OSError as e:
print(f"✗ No pude guardar: {e}")
if __name__ == "__main__":
main()
Lo nuevo:
- Persistencia con JSON. El menú se carga al inicio, se guarda al final.
- Historial append-only de pedidos en
pedidos.json. - Excepciones propias (
CodigoInvalido,StockInsuficiente) en lugar de strings de error. - Manejo robusto — archivo corrupto, archivo faltante, error de I/O — todo capturado.
pathlibpara manejo de rutas.
Si corrés el programa, lo cerrás y lo abrís otra vez, el inventario y los precios siguen donde los dejaste. Y pedidos.json tiene la historia completa. Eso es un programa real.
Próximo capítulo: lo refactorizamos con clases (Producto, Pupuseria, Pedido) para que el código sea más limpio y extensible.
4.11 Resumen visual
| Concepto | Una línea |
|---|---|
open(path, mode) |
Abre archivo. Modos: r, w, a, b |
with open(...) as f: |
Cierre automático garantizado |
f.read(), f.readlines(), for line in f |
Tres formas de leer |
f.write(...) |
Escribir (no agrega \n) |
pathlib.Path |
API moderna de paths |
csv.reader, csv.DictReader |
Leer CSV |
json.load, json.dump |
Cargar/guardar JSON |
try/except/else/finally |
Manejo de excepciones |
raise SomeError(...) |
Levantar excepción |
class MiError(Exception) |
Definir excepción propia |
| EAFP | Estilo Python: probar y capturar |
4.12 Ejercicios
✏️ Ejercicio 4.1 — Contar líneas y palabras
Escribí un programa que reciba un nombre de archivo y muestre cuántas líneas y cuántas palabras tiene.
Solución
import sys
from pathlib import Path
def stats(path):
p = Path(path)
try:
texto = p.read_text(encoding="utf-8")
except FileNotFoundError:
print(f"No existe: {path}")
return
lineas = texto.count("\n") + (0 if texto.endswith("\n") else 1)
palabras = len(texto.split())
print(f"{path}: {lineas} líneas, {palabras} palabras")
if __name__ == "__main__":
if len(sys.argv) != 2:
print("uso: stats.py archivo")
else:
stats(sys.argv[1])
✏️ Ejercicio 4.2 — Conversor CSV → JSON
Recibí un archivo CSV con encabezado y devolvé un JSON con la lista de filas como dicts.
Solución
import csv, json, sys
from pathlib import Path
def csv_a_json(csv_path, json_path):
with open(csv_path, encoding="utf-8") as f:
filas = list(csv.DictReader(f))
Path(json_path).write_text(
json.dumps(filas, indent=2, ensure_ascii=False),
encoding="utf-8",
)
csv_a_json("estudiantes.csv", "estudiantes.json")
Ese script de 5 líneas reemplaza todo lo que se hace en interfaces visuales como Excel.
✏️ Ejercicio 4.3 — Validador robusto
Pedile al usuario una nota (entre 0 y 10). Validá hasta que escriba algo válido. Manejá: cadena vacía, no es número, fuera de rango.
Solución
def pedir_nota():
while True:
s = input("Nota (0-10): ").strip()
if not s:
print(" No puede estar vacío")
continue
try:
n = float(s)
except ValueError:
print(" No es un número")
continue
if not 0 <= n <= 10:
print(" Fuera de rango")
continue
return n
print(pedir_nota())
✏️ Ejercicio 4.4 — Buscar en logs
Escribí función que recorra /var/log/syslog (o cualquier archivo grande) y cuente cuántas líneas contienen una palabra dada, sin cargar el archivo entero a memoria.
Solución
def contar_lineas_con(path, palabra):
n = 0
try:
with open(path, encoding="utf-8", errors="replace") as f:
for linea in f:
if palabra in linea:
n += 1
except (FileNotFoundError, PermissionError) as e:
print(f"Error: {e}")
return -1
return n
print(contar_lineas_con("/var/log/syslog", "error"))
errors="replace" evita explotar con bytes inválidos. for linea in f lee de a una.
4.13 Para profundizar
- Documentación oficial: https://docs.python.org/es/3/tutorial/inputoutput.html
pathlibcookbook: https://docs.python.org/es/3/library/pathlib.html- Excepciones: https://docs.python.org/es/3/tutorial/errors.html
- Próximo capítulo: Clases y OOP — el último gran tema antes de cerrar el libro.
Definiciones nuevas: archivo, modo de apertura, context manager, with, encoding, pathlib, CSV, JSON, excepción, try/except/else/finally, raise, EAFP, LBYL, jerarquía de excepciones, excepción personalizada.