Módulo 1 · Lección 48 de 59
Ficheros JSON
El formato en el que se intercambian datos en toda la web, y las cuatro funciones que lo convierten en estructuras de Python.
Un CSV sirve para una tabla: filas y columnas, todo plano. Pero la mayoría de los datos reales no son planos. Una configuración tiene secciones dentro de secciones, un usuario tiene una lista de direcciones y cada dirección tiene sus campos. Para eso hace falta un formato que admita estructuras anidadas, y el que ha ganado es JSON.
JSON viene de JavaScript Object Notation, aunque hoy no tiene nada que ver con JavaScript: lo hablan todos los lenguajes. Cuando una aplicación móvil pide datos a un servidor, cuando guardas la configuración de un programa, cuando consumes una API, hay JSON de por medio. Y si ya sabes manejar diccionarios y listas de Python, lo tienes aprendido casi entero, porque un JSON es exactamente eso escrito como texto.
Qué aspecto tiene#
{
"aplicacion": "Gestor de reservas",
"puerto": 8080,
"depuracion": false,
"administradores": ["lferrer", "mruiz"],
"correo": {
"servidor": "smtp.ejemplo.es",
"puerto": 587
}
}Llaves para las estructuras clave-valor, corchetes para las listas, comillas dobles para el texto. Igual que un diccionario de Python con listas y diccionarios dentro, con tres diferencias que se aprenden de golpe:
| Python | JSON | Nota |
|---|---|---|
dict | objeto {} | Las claves solo pueden ser texto |
list, tuple | array [] | Las tuplas se convierten en arrays |
str | string | Siempre con comillas dobles |
int | number | |
float | number | |
True | true | En minúscula |
False | false | En minúscula |
None | null |
Las cuatro funciones#
El módulo json tiene cuatro funciones que se resumen en dos ideas cruzadas: convertir en un sentido o en el otro, y trabajar con cadenas o con ficheros.
| Función | De qué a qué | Dónde |
|---|---|---|
json.dumps(datos) | Python a JSON | Devuelve una cadena |
json.loads(texto) | JSON a Python | Recibe una cadena |
json.dump(datos, fichero) | Python a JSON | Escribe en un fichero |
json.load(fichero) | JSON a Python | Lee de un fichero |
dumps y loads, con cadenas#
import json
configuracion = {
"nombre": "Panel de control",
"version": 3,
"activo": True,
"margen": 1.5,
"idiomas": ["es", "en", "gl"],
"tema": None,
}
texto = json.dumps(configuracion)
print(texto)
print(type(texto)){"nombre": "Panel de control", "version": 3, "activo": true, "margen": 1.5, "idiomas": ["es", "en", "gl"], "tema": null}
<class 'str'>Mira las traducciones: True ha pasado a true y None a null. Y lo que tienes ahora es una cadena, no un diccionario: no puedes hacer texto["version"].
El camino de vuelta es loads, y es el que usarás al recibir datos de una API:
import json
recibido = '{"usuario": "lferrer", "intentos": 3, "bloqueado": false}'
datos = json.loads(recibido)
print(datos)
print(type(datos))
print(datos["intentos"] + 1)
print(type(datos["bloqueado"])){'usuario': 'lferrer', 'intentos': 3, 'bloqueado': False}
<class 'dict'>
4
<class 'bool'>Ahora sí es un diccionario de verdad, con sus tipos recuperados: el 3 es un entero con el que se puede sumar y el false ha vuelto a ser False. Esa es la gran diferencia con el CSV, donde todo llegaba como texto.
dump y load, con ficheros#
import json
ajustes = {
"aplicacion": "Gestor de reservas",
"puerto": 8080,
"depuracion": False,
"administradores": ["lferrer", "mruiz"],
"correo": {
"servidor": "smtp.ejemplo.es",
"puerto": 587,
"remitente": "reservas@ejemplo.es",
},
}
with open("ajustes.json", "w", encoding="utf-8") as fichero:
json.dump(ajustes, fichero, indent=2, ensure_ascii=False)
with open("ajustes.json", encoding="utf-8") as fichero:
recuperado = json.load(fichero)
print(recuperado["correo"]["servidor"])
print(recuperado == ajustes)smtp.ejemplo.es
TrueEsa última línea es la que importa: lo que has recuperado es idéntico a lo que guardaste, con su estructura anidada y sus tipos intactos. Has guardado un diccionario en disco y lo has traído de vuelta en dos líneas.
Fíjate también en el orden de los argumentos de dump: primero los datos, después el fichero. Se confunde siempre.
Los dos parámetros que se ponen siempre#
indent, para que se pueda leer#
Sin indent, el JSON sale en una sola línea interminable. Con él, sale con sangría:
import json
configuracion = {"nombre": "Panel", "idiomas": ["es", "en"], "activo": True}
print(json.dumps(configuracion, indent=2)){
"nombre": "Panel",
"idiomas": [
"es",
"en"
],
"activo": true
}Usa indent=2 para cualquier fichero que una persona vaya a abrir alguna vez: configuraciones, datos de ejemplo, ficheros que van a un repositorio. Para datos que solo viajan entre máquinas, déjalo sin poner y ahorra bytes.
ensure_ascii=False, para los acentos#
Por defecto, json escapa cualquier carácter que no sea ASCII, y las tildes se convierten en códigos:
import json
datos = {"ciudad": "Alcalá de Henares", "región": "Madrid"}
print(json.dumps(datos))
print(json.dumps(datos, ensure_ascii=False)){"ciudad": "Alcal\u00e1 de Henares", "regi\u00f3n": "Madrid"}
{"ciudad": "Alcalá de Henares", "región": "Madrid"}Las dos líneas son JSON válido y las dos se leen igual de bien: \u00e1 es una á, solo que escrita en su forma escapada. La diferencia es para el ojo humano. Como hoy todo el mundo usa UTF-8, pon ensure_ascii=False y guarda las tildes tal cual.
Qué no se puede convertir#
JSON solo conoce los tipos de la tabla de arriba. Todo lo demás falla.
Las tuplas son un caso especial: sí se convierten, pero pierden su identidad y vuelven como listas.
import json
print(json.dumps({"punto": (3, 4)})){"punto": [3, 4]}Los conjuntos y las fechas no se convierten en absoluto:
import json
print(json.dumps({"es", "en"}))TypeError: Object of type set is not JSON serializableCuando el fichero está mal formado#
Este JSON tiene un fallo, y es el más frecuente de todos:
{
"aplicacion": "Gestor de reservas",
"puerto": 8080,
"depuracion": false,
}La coma después de false sobra. En Python una coma final es perfectamente legal; en JSON, no.
json.decoder.JSONDecodeError: Illegal trailing comma before end of object: line 4 column 22 (char 79)Un fichero de preferencias#
El caso de uso más común de JSON en un programa de escritorio: recordar las preferencias entre ejecuciones, partiendo de unas de fábrica la primera vez.
import json
from pathlib import Path
RUTA = Path(__file__).parent / "preferencias.json"
PREFERENCIAS_POR_DEFECTO = {
"tema": "claro",
"idioma": "es",
"tamano_letra": 14,
"avisos": True,
"carpetas_recientes": [],
}
def cargar_preferencias() -> dict:
"""Devuelve las preferencias guardadas, o las de fábrica si no hay fichero."""
if not RUTA.exists():
return dict(PREFERENCIAS_POR_DEFECTO)
with open(RUTA, encoding="utf-8") as fichero:
return json.load(fichero)
def guardar_preferencias(preferencias: dict) -> None:
"""Escribe las preferencias en disco de forma legible."""
with open(RUTA, "w", encoding="utf-8") as fichero:
json.dump(preferencias, fichero, indent=2, ensure_ascii=False)
preferencias = cargar_preferencias()
print("Al arrancar:", preferencias["tema"], preferencias["tamano_letra"])
preferencias["tema"] = "oscuro"
preferencias["tamano_letra"] = 16
preferencias["carpetas_recientes"].append("/Users/erwin/informes año 2026")
guardar_preferencias(preferencias)
recuperadas = cargar_preferencias()
print("Tras recargar:", recuperadas["tema"], recuperadas["tamano_letra"])
print("Recientes:", recuperadas["carpetas_recientes"])La primera vez que lo ejecutas, el fichero no existe y salen las preferencias de fábrica:
Al arrancar: claro 14
Tras recargar: oscuro 16
Recientes: ['/Users/erwin/informes año 2026']Y deja esto en el disco:
{
"tema": "oscuro",
"idioma": "es",
"tamano_letra": 16,
"avisos": true,
"carpetas_recientes": [
"/Users/erwin/informes año 2026"
]
}Si lo vuelves a ejecutar, la primera línea ya dirá oscuro 16: las preferencias han sobrevivido al cierre del programa.
Dos decisiones del diseño que merece la pena imitar. cargar_preferencias() devuelve dict(PREFERENCIAS_POR_DEFECTO), una copia, y no el diccionario original: si devolviera el original, modificar las preferencias en memoria cambiaría también las de fábrica. Y la ruta cuelga de Path(__file__).parent, para que el fichero se guarde junto al programa y no en la carpeta desde la que se lanzó.