Saltar al contenido
ficheros.py · devschool

Ficheros y JSON en Python

Lección 15 de 19 · 12 min de lectura · Actualizado el

En esta lección
  1. Abrir un fichero: open y with
  2. Modos de apertura
  3. Leer un fichero de texto
  4. Escribir en un fichero
  5. Rutas con pathlib
  6. Ficheros CSV
  7. JSON
  8. Cuando el fichero no existe
  9. Ejemplo completo: guardar y cargar una agenda
  10. Errores frecuentes
  11. Resumen

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

ModoSignificadoSi el fichero existeSi no existe
"r"Leer (por defecto)Lo leeFileNotFoundError
"w"EscribirLo vacía y escribe desde ceroLo crea
"a"Añadir (append)Escribe al final, conserva lo que habíaLo crea
"x"CrearFileExistsErrorLo 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:

JSONPython
objeto {}dict
array []list
cadena "..."str
númeroint o float
true / falseTrue / False
nullNone

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 dan TypeError: Object of type set is not JSON serializable. Las fechas se suelen guardar como texto con isoformat() (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 o UnicodeDecodeError en otro sistema.
  • Usar "w" en lugar de "a": borra todo lo anterior.
  • Olvidar el \n con write: 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" + 1 da TypeError; convierte con float().
  • Confundir dump con dumps: dump escribe en un fichero; dumps devuelve 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

TareaCódigo
Abrir con cierre automáticowith open(ruta, modo, encoding="utf-8") as f:
Leer todof.read() o Path(ruta).read_text(encoding="utf-8")
Recorrer líneasfor linea in f:
Escribirf.write(texto) (añade tú el \n)
Añadir al finalmodo "a"
Unir rutasPath("carpeta") / "archivo.txt"
Crear carpetaPath(ruta).mkdir(parents=True, exist_ok=True)
Leer CSV como diccionarioscsv.DictReader(f)
Escribir CSVcsv.writer(f) o csv.DictWriter(f, fieldnames=...)
Guardar JSONjson.dump(datos, f, indent=2, ensure_ascii=False)
Cargar JSONjson.load(f)
Texto a JSON y al revésjson.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

[Python] ¿Qué imprime este código?
with open("t.txt", "w", encoding="utf-8") as f:
    f.write("uno\n")
with open("t.txt", "w", encoding="utf-8") as f:
    f.write("dos\n")
with open("t.txt", "a", encoding="utf-8") as f:
    f.write("tres\n")

print(open("t.txt", encoding="utf-8").read().splitlines())

[Python] ¿Cuál es la principal ventaja de abrir un fichero con with open(...) as f?

[Python] ¿Qué diferencia hay entre json.dump y json.dumps?

[Python] Lees un CSV con csv.DictReader y haces fila["nota"] + 1. ¿Qué ocurre?

¿Te ha quedado claro? Márcala y verás tu progreso en el explorador.