Ficheros y JSON en Python
En esta lección
Hasta ahora, todo lo que guardaban tus programas desaparecía al cerrarlos. Para que los datos persistan (una lista de tareas, las notas de una clase, el inventario de una tienda) hay que escribirlos en ficheros. En esta lección aprenderás a leer y escribir ficheros de texto con open y with, a manejar rutas con pathlib y a trabajar con los dos formatos de datos más usados: CSV y JSON. Terminaremos con una agenda de contactos que se guarda y se carga sola.
Abrir un fichero: open y with
La función open abre un fichero y devuelve un objeto con el que leer o escribir. La forma correcta de usarla es dentro de un bloque with:
with open("tareas.txt", "w", encoding="utf-8") as f:
f.write("Estudiar Python\n")
f.write("Hacer la compra\n")
f.write("Llamar a Lucía\n")
Tiene tres partes:
- La ruta del fichero:
"tareas.txt"(relativa a la carpeta desde la que ejecutas el programa). - El modo:
"w"para escribir. Lo vemos enseguida. - La codificación:
encoding="utf-8".
¿Por qué with?
Un fichero abierto consume recursos del sistema y, al escribir, los datos no siempre llegan al disco hasta que se cierra. with cierra el fichero automáticamente al salir del bloque, incluso si se produce un error dentro.
f = open("tareas.txt", encoding="utf-8")
print(f.closed) # False
f.close() # sin with, tienes que acordarte de cerrarlo
print(f.closed) # True
Con with no hay nada que recordar. Úsalo siempre.
¿Por qué encoding="utf-8"?
Si no indicas la codificación, Python usa la del sistema operativo. En Windows suele ser una distinta de UTF-8, así que un fichero con “Lucía” o ”€” escrito en Linux puede leerse mal en Windows (o dar UnicodeDecodeError). Indicar siempre encoding="utf-8" hace que tu programa se comporte igual en todos los equipos.
Modos de apertura
| Modo | Significado | Si el fichero existe | Si no existe |
|---|---|---|---|
"r" | Leer (por defecto) | Lo lee | FileNotFoundError |
"w" | Escribir | Lo vacía y escribe desde cero | Lo crea |
"a" | Añadir (append) | Escribe al final, conserva lo que había | Lo crea |
"x" | Crear | FileExistsError | Lo crea |
"b" | Binario, se combina: "rb", "wb" |
Cuidado: el modo
"w"borra el contenido anterior sin preguntar. Si quieres añadir líneas a un fichero existente, usa"a". Si quieres asegurarte de no machacar nada, usa"x".
El modo "b" se usa para ficheros que no son texto (imágenes, PDF, ZIP). En ese caso no se indica encoding y se trabaja con bytes:
with open("logo.png", "rb") as f:
cabecera = f.read(4)
print(cabecera) # b'\x89PNG'
Leer un fichero de texto
Con el tareas.txt que acabamos de crear, hay varias formas de leer.
read(): todo de golpe
with open("tareas.txt", encoding="utf-8") as f:
contenido = f.read()
print(repr(contenido))
# 'Estudiar Python\nHacer la compra\nLlamar a Lucía\n'
Devuelve una sola cadena con todo el contenido, incluidos los saltos de línea \n. Cómodo para ficheros pequeños.
readline(): una línea cada vez
with open("tareas.txt", encoding="utf-8") as f:
print(repr(f.readline())) # 'Estudiar Python\n'
print(repr(f.readline())) # 'Hacer la compra\n'
El fichero recuerda por dónde va: cada llamada devuelve la siguiente línea. Al llegar al final devuelve "".
readlines(): lista de líneas
with open("tareas.txt", encoding="utf-8") as f:
print(f.readlines())
# ['Estudiar Python\n', 'Hacer la compra\n', 'Llamar a Lucía\n']
Recorrer las líneas con for (la mejor opción)
Un fichero abierto se puede recorrer directamente, línea a línea. Es la forma más habitual y la más eficiente, porque no carga todo el fichero en memoria:
with open("tareas.txt", encoding="utf-8") as f:
for numero, linea in enumerate(f, start=1):
print(f"{numero}. {linea.strip()}")
# 1. Estudiar Python
# 2. Hacer la compra
# 3. Llamar a Lucía
strip() elimina el \n del final de cada línea. Sin él, print añadiría un salto más y verías líneas en blanco entre medias.
Escribir en un fichero
write escribe una cadena y no añade salto de línea: tienes que ponerlo tú.
with open("tareas.txt", "a", encoding="utf-8") as f:
f.write("Ir al gimnasio\n") # se añade al final
writelines escribe una secuencia de cadenas, pero tampoco añade saltos:
compra = ["pan", "leche", "huevos"]
with open("compra.txt", "w", encoding="utf-8") as f:
f.writelines(compra) # escribe: panlechehuevos
with open("compra.txt", "w", encoding="utf-8") as f:
f.writelines(p + "\n" for p in compra) # una por línea
También puedes usar print con el parámetro file, que sí añade el salto:
with open("registro.txt", "a", encoding="utf-8") as f:
print("Usuario ana ha iniciado sesión", file=f)
Recuerda que write solo acepta texto: para guardar un número, conviértelo antes con str() o usa un f-string.
Rutas con pathlib
Escribir rutas como cadenas da problemas: Windows usa \ y Linux y macOS usan /. El módulo pathlib representa las rutas como objetos Path y se encarga de esas diferencias:
from pathlib import Path
carpeta = Path("informes")
ruta = carpeta / "resumen.txt" # el operador / une rutas
print(ruta.name, ruta.stem, ruta.suffix) # resumen.txt resumen .txt
print(ruta.parent) # informes
Comprobar y crear
print(carpeta.exists()) # False (todavía no existe)
carpeta.mkdir(exist_ok=True) # la crea; si ya existe, no da error
Path("informes/2025/enero").mkdir(parents=True, exist_ok=True) # crea toda la ruta
print(carpeta.is_dir(), ruta.is_file())
Leer y escribir de una vez
Para ficheros pequeños, Path tiene atajos que abren, leen o escriben y cierran en una sola línea:
ruta.write_text("Ventas: 1500 €\nClientes: 32\n", encoding="utf-8")
texto = ruta.read_text(encoding="utf-8")
print(texto.splitlines()) # ['Ventas: 1500 €', 'Clientes: 32']
Buscar ficheros con glob
for fichero in sorted(carpeta.glob("*.txt")):
print(fichero)
# informes/notas.txt
# informes/resumen.txt
glob("*.txt") busca en la carpeta; rglob("*.txt") busca también en todas las subcarpetas. Además, open acepta objetos Path igual que cadenas.
Ficheros CSV
Un CSV (comma-separated values) es una tabla en texto plano: cada línea es una fila y las columnas se separan por comas. Lo abren Excel, LibreOffice y Google Sheets. El módulo csv se ocupa de los detalles, como los valores que contienen comas.
Al abrir un CSV, pasa siempre newline="": así el módulo csv controla los saltos de línea y evitas filas en blanco en Windows.
Escribir con writer
import csv
alumnos = [
["nombre", "curso", "nota"],
["Ana", "1DAW", 8.5],
["Luis", "1DAW", 6],
["Marta", "2DAW", 9.2],
]
with open("alumnos.csv", "w", newline="", encoding="utf-8") as f:
escritor = csv.writer(f)
escritor.writerows(alumnos) # writerow() para una sola fila
nombre,curso,nota
Ana,1DAW,8.5
Luis,1DAW,6
Marta,2DAW,9.2
Leer con reader
with open("alumnos.csv", newline="", encoding="utf-8") as f:
lector = csv.reader(f)
cabecera = next(lector) # la primera fila
for fila in lector:
print(fila)
# ['Ana', '1DAW', '8.5']
# ['Luis', '1DAW', '6']
# ['Marta', '2DAW', '9.2']
Fíjate: todo llega como texto. Para operar con la nota hay que convertirla con float().
DictReader y DictWriter
Más cómodo todavía: DictReader usa la cabecera para darte cada fila como un diccionario, así accedes por nombre de columna en lugar de por posición:
with open("alumnos.csv", newline="", encoding="utf-8") as f:
notas = [float(fila["nota"]) for fila in csv.DictReader(f)]
print(round(sum(notas) / len(notas), 2)) # 7.9
Y DictWriter escribe a partir de diccionarios:
productos = [
{"nombre": "Teclado", "precio": 25.0, "stock": 10},
{"nombre": "Ratón, inalámbrico", "precio": 12.5, "stock": 0},
]
with open("productos.csv", "w", newline="", encoding="utf-8") as f:
escritor = csv.DictWriter(f, fieldnames=["nombre", "precio", "stock"])
escritor.writeheader()
escritor.writerows(productos)
nombre,precio,stock
Teclado,25.0,10
"Ratón, inalámbrico",12.5,0
El nombre con coma se ha puesto entre comillas automáticamente. Si tu CSV usa punto y coma (habitual en Excel en español), añade delimiter=";" a reader, writer, DictReader o DictWriter.
JSON
JSON es el formato estándar para intercambiar datos entre programas y con las APIs web. Se parece mucho a los diccionarios y listas de Python:
| JSON | Python |
|---|---|
objeto {} | dict |
array [] | list |
cadena "..." | str |
| número | int o float |
true / false | True / False |
null | None |
dumps y loads: con cadenas
dumps (dump string) convierte datos de Python en una cadena JSON; loads hace lo contrario:
import json
alumno = {"nombre": "Lucía", "edad": 20, "activo": True, "notas": [8.5, 7], "tutor": None}
print(json.dumps(alumno))
# {"nombre": "Lucía", "edad": 20, "activo": true, "notas": [8.5, 7], "tutor": null}
print(json.dumps(alumno, ensure_ascii=False))
# {"nombre": "Lucía", "edad": 20, "activo": true, "notas": [8.5, 7], "tutor": null}
datos = json.loads('{"producto": "Café", "precio": 4.5, "disponible": false}')
print(datos["precio"] * 2) # 9.0
Por defecto, JSON escapa las tildes y la ñ (í). Con ensure_ascii=False se guardan tal cual, que es mucho más legible.
dump y load: con ficheros
Sin la s, trabajan directamente con un fichero abierto:
with open("alumno.json", "w", encoding="utf-8") as f:
json.dump(alumno, f, indent=2, ensure_ascii=False)
with open("alumno.json", encoding="utf-8") as f:
cargado = json.load(f)
print(cargado == alumno) # True
indent=2 sangra el resultado para que un humano pueda leerlo:
{
"nombre": "Lucía",
"edad": 20,
"activo": true,
"notas": [
8.5,
7
],
"tutor": null
}
Cuidado: JSON no admite todos los tipos de Python. Las tuplas se convierten en listas, las claves numéricas en cadenas (
{1: "a"}vuelve como{"1": "a"}) y los conjuntos o las fechas danTypeError: Object of type set is not JSON serializable. Las fechas se suelen guardar como texto conisoformat()(lo verás en fechas).
Si el texto no es JSON válido (por ejemplo, con comillas simples), json.loads lanza json.JSONDecodeError.
Cuando el fichero no existe
Abrir para leer un fichero que no existe lanza FileNotFoundError. Puedes capturarlo con lo que aprendiste en excepciones:
try:
with open("notas_2020.txt", encoding="utf-8") as f:
print(f.read())
except FileNotFoundError:
print("No existe el fichero de notas")
O comprobarlo antes con Path("notas_2020.txt").exists(). En Python se prefiere el try, porque el fichero podría desaparecer entre la comprobación y la apertura.
Ejemplo completo: guardar y cargar una agenda
Juntemos todo en un pequeño programa que guarda contactos en agenda.json y los recupera al volver a ejecutarse:
import json
from pathlib import Path
RUTA = Path("agenda.json")
def cargar_agenda():
"""Devuelve la lista de contactos guardada, o una lista vacía."""
try:
with open(RUTA, encoding="utf-8") as f:
return json.load(f)
except FileNotFoundError:
return []
except json.JSONDecodeError:
print("Aviso: agenda.json está dañado; se empieza de cero.")
return []
def guardar_agenda(contactos):
with open(RUTA, "w", encoding="utf-8") as f:
json.dump(contactos, f, indent=2, ensure_ascii=False)
def agregar_contacto(contactos, nombre, telefono, email=""):
contactos.append({"nombre": nombre, "telefono": telefono, "email": email})
guardar_agenda(contactos)
def buscar(contactos, texto):
texto = texto.lower()
return [c for c in contactos if texto in c["nombre"].lower()]
if __name__ == "__main__":
agenda = cargar_agenda()
print(f"Contactos cargados: {len(agenda)}")
agregar_contacto(agenda, "Lucía Gómez", "600111222", "lucia@correo.es")
agregar_contacto(agenda, "Íñigo Ruiz", "611333444")
for c in buscar(agenda, "lu"):
print(f"{c['nombre']}: {c['telefono']}")
La primera ejecución muestra:
Contactos cargados: 0
Lucía Gómez: 600111222
Y deja un agenda.json legible, con tildes. Si lo ejecutas otra vez, verás Contactos cargados: 2: los datos han sobrevivido al cierre del programa (y se añadirán dos contactos repetidos, porque el ejemplo siempre agrega los mismos). Fíjate en las decisiones de diseño: cada función hace una sola cosa, un fichero inexistente o dañado no rompe el programa y se guarda después de cada cambio para no perder nada.
Errores frecuentes
- Olvidar
encoding="utf-8": tildes y eñes estropeadas oUnicodeDecodeErroren otro sistema. - Usar
"w"en lugar de"a": borra todo lo anterior. - Olvidar el
\nconwrite: todo acaba en una sola línea. - No quitar el salto de línea al leer: usa
linea.strip(). - Olvidar que CSV devuelve texto:
"8.5" + 1daTypeError; convierte confloat(). - Confundir
dumpcondumps:dumpescribe en un fichero;dumpsdevuelve una cadena. - Rutas relativas que no encuentran el fichero: son relativas a la carpeta desde la que ejecutas el programa, no a la del script.
Resumen
| Tarea | Código |
|---|---|
| Abrir con cierre automático | with open(ruta, modo, encoding="utf-8") as f: |
| Leer todo | f.read() o Path(ruta).read_text(encoding="utf-8") |
| Recorrer líneas | for linea in f: |
| Escribir | f.write(texto) (añade tú el \n) |
| Añadir al final | modo "a" |
| Unir rutas | Path("carpeta") / "archivo.txt" |
| Crear carpeta | Path(ruta).mkdir(parents=True, exist_ok=True) |
| Leer CSV como diccionarios | csv.DictReader(f) |
| Escribir CSV | csv.writer(f) o csv.DictWriter(f, fieldnames=...) |
| Guardar JSON | json.dump(datos, f, indent=2, ensure_ascii=False) |
| Cargar JSON | json.load(f) |
| Texto a JSON y al revés | json.dumps(datos), json.loads(texto) |
Con ficheros y JSON ya puedes crear programas que recuerdan sus datos. En la próxima lección organizarás esos datos y su comportamiento con clases y objetos.
Pon a prueba lo que has aprendido
¿Te ha quedado claro? Márcala y verás tu progreso en el explorador.