JSON en Python: leer y escribir archivos, CSV, pathlib y codificación UTF-8
Para trabajar con JSON en Python existe el módulo integrado json: json.load lee los datos de un archivo abierto, json.dump los escribe en un archivo, y json.loads y json.dumps hacen lo mismo con cadenas. El archivo se abre con with open(ruta, encoding="utf-8"), las rutas se construyen con pathlib.Path y las tablas se leen con el módulo csv.
Trabajar con JSON en Python no requiere bibliotecas externas: el módulo json forma parte de la biblioteca estándar y convierte diccionarios y listas en texto y viceversa. Junto a él casi siempre hacen falta otras tres cosas: la función open con la sentencia with, el módulo pathlib para las rutas y el módulo csv para las tablas. A continuación lo vemos todo en orden, con código que funciona y con las trampas en las que más se tropieza.
La función open en Python: modos y codificación
Cualquier archivo de texto se abre con la función integrada open(ruta, modo, encoding=...). Devuelve un objeto de archivo con los métodos read, readline y write, y que se puede recorrer línea a línea en un bucle.
| Modo | Qué hace |
|---|---|
"r" | Lectura (por defecto). Si el archivo no existe, FileNotFoundError |
"w" | Escritura. El archivo se crea o se vacía en cuanto se abre |
"a" | Añadir al final; el contenido existente no se toca |
"x" | Crear un archivo nuevo; si ya existe, FileExistsError |
"b" | Complemento del modo: trabajo con bytes ("rb", "wb") |
Para qué sirve with
Los archivos hay que cerrarlos: si no, los datos pueden no llegar al disco y el sistema mantiene ocupado el descriptor. La sentencia with cierra el archivo automáticamente, incluso si dentro del bloque se produce un error.
with open("notas.txt", "w", encoding="utf-8") as f:
f.write("Primera línea\n")
f.write("Segunda línea\n")
with open("notas.txt", encoding="utf-8") as f:
for linea in f:
print(linea.rstrip("\n"))
Recorrer el archivo en un bucle lee una línea cada vez y no carga todo en memoria, así que de este modo se pueden procesar registros de gigabytes. El método f.read() devuelve todo el contenido en una sola cadena, y f.readlines(), una lista de líneas.
Cómo escribir una línea sin borrar lo anterior
El error más frecuente de los principiantes es abrir el archivo en modo "w" para «añadir» una línea. El modo "w" vacía el archivo en el momento de abrirlo, antes de la primera escritura. Para añadir al final use "a", y para no sobrescribir nunca un archivo ajeno, "x".
with open("registro.txt", "a", encoding="utf-8") as f:
f.write("nueva entrada\n")
Tenga en cuenta que write no añade el salto de línea por sí mismo: el "\n" se escribe de forma explícita. También se puede usar print(..., file=f), que pone el salto por usted.
Codificación UTF-8: de dónde salen los caracteres ilegibles
Hasta Python 3.15, la función open() sin el parámetro encoding usaba la codificación del sistema. En Linux y macOS es UTF-8, pero en Windows con configuración regional en ruso era cp1251. Un script que funcionaba bien en el servidor mostraba en el portátil de un compañero algo como «Café» en lugar de «Café», o fallaba con UnicodeDecodeError.
En Python 3.15, según la PEP 686, el modo UTF-8 está activado por defecto: open() sin encoding lee y escribe UTF-8 en todos los sistemas operativos. Para volver al comportamiento anterior se puede usar la variable de entorno PYTHONUTF8=0 o el indicador -X utf8=0, y para pedir expresamente la codificación del sistema, encoding="locale".
import sys
print(sys.flags.utf8_mode) # 1 en Python 3.15
open("heredado.txt", encoding="locale") # codificación del sistema
open("excel_antiguo.csv", encoding="cp1251") # archivo guardado en cp1251
El reverso de la moneda: un CSV antiguo de Excel o de un script de Windows, guardado en cp1251, ahora se leerá como UTF-8 y provocará UnicodeDecodeError. La solución es leerlo con encoding="cp1251" y volver a guardarlo en UTF-8. La buena práctica no cambia: indique encoding siempre de forma explícita, y el código se comportará igual en cualquier versión.
pathlib en Python: rutas como objetos
Unir rutas con cadenas ("datos" + "/" + nombre) es incómodo y se rompe entre sistemas operativos. El módulo pathlib ofrece la clase Path, en la que las rutas se unen con el operador / y los objetos tienen propiedades claras.
from pathlib import Path
datos = Path("datos")
ruta_csv = datos / "gastos.csv"
print(ruta_csv.name) # gastos.csv
print(ruta_csv.stem) # gastos
print(ruta_csv.suffix) # .csv
print(ruta_csv.parent) # datos
print(ruta_csv.exists())
datos.mkdir(parents=True, exist_ok=True)
(datos / "hola.txt").write_text("Hola", encoding="utf-8")
print((datos / "hola.txt").read_text(encoding="utf-8"))
for p in datos.glob("*.csv"):
print(p)
Los métodos read_text y write_text sustituyen la combinación open + with para archivos pequeños. Un objeto Path se puede pasar tanto a open como a json o a csv; todos lo admiten. Para obtener una ruta relativa al propio script y no a la carpeta actual se escribe Path(__file__).parent / "datos".
En Python 3.14, Path ganó los métodos copy() y move(), así que para copiar de forma sencilla ya no hace falta shutil. En esa misma versión se añadió a la biblioteca estándar el módulo compression.zstd para la compresión Zstandard (PEP 784).
JSON en Python: load, dump, loads, dumps
JSON es un formato de texto para intercambiar datos: lo entregan las API y en él se guardan configuraciones e informes. El módulo json hace dos operaciones: serialización (objeto de Python → texto JSON) y deserialización (texto → objeto). Son cuatro funciones, y confundirlas es una pregunta muy frecuente.
| Función | Qué hace |
|---|---|
json.dumps(obj) | Objeto → cadena JSON |
json.loads(s) | Cadena JSON → objeto |
json.dump(obj, f) | Objeto → escribir en un archivo abierto |
json.load(f) | Leer JSON de un archivo abierto |
Para recordarlo: la s final significa string, es decir, trabajo con una cadena; sin ella, trabajo con un archivo.
Correspondencia de tipos
| Python | JSON |
|---|---|
dict | objeto {} |
list, tuple | matriz [] |
str | cadena |
int, float | número |
True / False | true / false |
None | null |
Al leer de nuevo, una tupla se convierte en lista, pero el módulo no sabe trabajar con conjuntos, fechas ni Decimal, y produce TypeError: Object of type ... is not JSON serializable.
Cómo guardar JSON en un archivo
import json
from pathlib import Path
informe = {
"mes": "2026-10",
"total": 15230.5,
"categorias": {"comida": 8200, "transporte": 2100},
"etiquetas": ["casa", "trabajo"],
}
ruta = Path("datos/informe.json")
ruta.parent.mkdir(parents=True, exist_ok=True)
with ruta.open("w", encoding="utf-8") as f:
json.dump(informe, f, ensure_ascii=False, indent=2)
El parámetro ensure_ascii=False guarda los acentos tal cual. Sin él, en el archivo aparecería "comida" escrito con secuencias \u, lo cual es JSON correcto, pero ilegible para una persona. indent=2 aplica un formato con sangrías.
Cómo leer JSON de un archivo
with open("datos/informe.json", encoding="utf-8") as f:
datos = json.load(f)
print(datos["total"]) # 15230.5
print(datos["categorias"]["comida"]) # 8200
El resultado es un diccionario o una lista normales, con los que se trabaja con las herramientas habituales. Si el archivo no es JSON o está cortado, se produce la excepción json.JSONDecodeError con el número de línea y de posición; conviene capturarla cuando los datos llegan desde fuera.
Análisis de una cadena JSON y salida con formato
import json
bruto = '{"nombre": "Ana", "habilidades": ["python", "sql"], "activo": true}'
usuario = json.loads(bruto)
print(usuario["habilidades"][0]) # python
print(json.dumps(usuario, ensure_ascii=False, indent=2, sort_keys=True))
sort_keys=True ordena las claves al mostrarlas, lo que facilita comparar archivos en git. Para dar formato a un archivo desde la terminal existe una utilidad integrada: python -m json.tool datos/informe.json.
La respuesta de una API HTTP también es una cadena JSON. La biblioteca requests la analiza con el método response.json(), y por dentro es el mismo json.loads.
Trampa: las claves de JSON siempre son cadenas
d = {1: "enero", 2: "febrero"}
vuelta = json.loads(json.dumps(d))
print(vuelta == d) # False
print(vuelta) # {'1': 'enero', '2': 'febrero'}
# vuelta[1] -> KeyError, hay que usar vuelta["1"]
corregido = {int(k): v for k, v in vuelta.items()}
En JSON, las claves de un objeto solo pueden ser cadenas, así que las claves numéricas se convierten en cadenas después de un ciclo de serialización. Al leer, vuelva a convertirlas.
Tipos propios: fechas y Decimal
import json
from datetime import date
def valor_por_defecto(obj):
if isinstance(obj, date):
return obj.isoformat()
raise TypeError(f"no sé serializar {type(obj).__name__}")
print(json.dumps({"dia": date(2026, 10, 1)}, default=valor_por_defecto))
# {"dia": "2026-10-01"}
La función del parámetro default se llama con cada objeto desconocido. Para casos complejos se puede heredar de json.JSONEncoder y sobrescribir el método default.
Cómo añadir un elemento a un array JSON
En JSON no se puede añadir al final como en un registro de texto: hay que leer el archivo, modificarlo y escribirlo completo.
ruta = Path("datos/elementos.json")
elementos = json.loads(ruta.read_text(encoding="utf-8")) if ruta.exists() else []
elementos.append({"id": len(elementos) + 1, "titulo": "nuevo"})
ruta.write_text(json.dumps(elementos, ensure_ascii=False, indent=2), encoding="utf-8")
Si hay muchos registros que se añaden de forma continua, conviene el formato JSON Lines: un objeto JSON por línea, y el archivo se abre en modo "a".
CSV en Python: el módulo csv
CSV es una tabla en un archivo de texto: las filas son registros y los valores van separados por comas. El módulo csv gestiona bien las comillas, las comas dentro de los valores y los saltos de línea, algo que no hace un split(",") manual.
import csv
with open("datos/gastos.csv", newline="", encoding="utf-8") as f:
lector = csv.DictReader(f)
total = 0.0
for fila in lector:
total += float(fila["importe"])
print(fila["fecha"], fila["categoria"], fila["importe"])
print("Total:", total)
csv.DictReader convierte cada línea en un diccionario según la cabecera del archivo. Todos los valores son cadenas: los números y las fechas hay que convertirlos (float, int, date.fromisoformat). Un archivo con separador «;» (así lo guarda Excel en muchas configuraciones regionales europeas) se lee con el parámetro delimiter=";".
filas = [
{"fecha": "2026-10-01", "categoria": "comida", "importe": 450},
{"fecha": "2026-10-02", "categoria": "transporte", "importe": 60},
]
with open("datos/salida.csv", "w", newline="", encoding="utf-8") as f:
escritor = csv.DictWriter(f, fieldnames=["fecha", "categoria", "importe"])
escritor.writeheader()
escritor.writerows(filas)
Al escribir es obligatorio newline="": sin él, en Windows aparecerían líneas vacías entre las filas. Si el archivo se va a abrir en Excel, use encoding="utf-8-sig": la marca BOM ayuda a Excel a reconocer UTF-8.
De CSV a JSON: ejemplo completo
import csv
import json
from collections import defaultdict
from pathlib import Path
origen = Path("datos/gastos.csv")
destino = Path("datos/informe.json")
por_categoria = defaultdict(float)
with origen.open(newline="", encoding="utf-8") as f:
for fila in csv.DictReader(f):
por_categoria[fila["categoria"]] += float(fila["importe"])
informe = {"total": sum(por_categoria.values()), "categorias": dict(por_categoria)}
destino.write_text(json.dumps(informe, ensure_ascii=False, indent=2), encoding="utf-8")
print(destino.read_text(encoding="utf-8"))
Este es el núcleo de cualquier script de informes: entrada desde CSV, agregación en un diccionario y salida en JSON.
Errores típicos
FileNotFoundError: la ruta es relativa y se calcula desde la carpeta desde la que se lanza el programa, no desde el script. UsePath(__file__).parent.- El archivo se ha vaciado: se abrió en
"w"en lugar de"a". UnicodeDecodeError: el archivo tiene otra codificación; indíquela de forma explícita (cp1251,utf-8-sig).TypeError: Object of type set is not JSON serializable: convierta el conjunto en lista o pasedefault.KeyErrordespués de cargar JSON: las claves numéricas se convirtieron en cadenas.- Un archivo propio se llama
json.pyocsv.py: tapa el módulo estándar yimport jsonimporta ese archivo.
Resumen
Para JSON en Python basta con la biblioteca estándar: json.load y json.dump para archivos, loads y dumps para cadenas, ensure_ascii=False e indent para una salida legible. Abra los archivos con with y con la codificación indicada, construya las rutas con pathlib y lea las tablas con csv.DictReader. En el curso de Python este tema se practica en la práctica: el registro de gastos lee los importes de un CSV y guarda el informe en JSON.
Preguntas frecuentes
¿En qué se diferencia json.load de json.loads?
json.load lee JSON de un objeto de archivo abierto, y json.loads analiza una cadena. De forma análoga, json.dump escribe en un archivo y json.dumps devuelve una cadena.
¿Cómo guardar acentos en JSON sin secuencias \u?
Pase ensure_ascii=False a json.dump o a json.dumps y abra el archivo con encoding="utf-8".
¿Hace falta instalar una biblioteca para trabajar con JSON en Python?
No, el módulo json está integrado en la biblioteca estándar; basta con escribir import json.
¿Se pueden guardar fechas o conjuntos en JSON?
No directamente: hay que convertirlos, por ejemplo una fecha en cadena ISO con isoformat, y un conjunto en lista; también se puede pasar una función al parámetro default.
¿Cómo leer un JSON grande que no cabe en memoria?
El json.load estándar lee el archivo completo; para volúmenes grandes conviene guardar los datos en formato JSON Lines y analizar cada línea con json.loads.
Este tema se explica a fondo y con práctica en el curso «Python 3.15: от новичка до профи».
Ir al curso