Saltar al contenido
🐍 Curso de Python
/

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.

⏱ 13 min de lectura

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#

ajustes.json
{
  "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:

PythonJSONNota
dictobjeto {}Las claves solo pueden ser texto
list, tuplearray []Las tuplas se convierten en arrays
strstringSiempre con comillas dobles
intnumber
floatnumber
TruetrueEn minúscula
FalsefalseEn minúscula
Nonenull

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ónDe qué a quéDónde
json.dumps(datos)Python a JSONDevuelve una cadena
json.loads(texto)JSON a PythonRecibe una cadena
json.dump(datos, fichero)Python a JSONEscribe en un fichero
json.load(fichero)JSON a PythonLee de un fichero

dumps y loads, con cadenas#

python
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))
Salida en pantalla
{"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:

python
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"]))
Salida en pantalla
{'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#

ajustes.py
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)
Salida en pantalla
smtp.ejemplo.es
True

Esa ú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:

python
import json

configuracion = {"nombre": "Panel", "idiomas": ["es", "en"], "activo": True}
print(json.dumps(configuracion, indent=2))
Salida en pantalla
{
  "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:

python
import json

datos = {"ciudad": "Alcalá de Henares", "región": "Madrid"}

print(json.dumps(datos))
print(json.dumps(datos, ensure_ascii=False))
Salida en pantalla
{"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.

python
import json

print(json.dumps({"punto": (3, 4)}))
Salida en pantalla
{"punto": [3, 4]}

Los conjuntos y las fechas no se convierten en absoluto:

python
import json

print(json.dumps({"es", "en"}))
Salida en pantalla
TypeError: Object of type set is not JSON serializable

Cuando el fichero está mal formado#

Este JSON tiene un fallo, y es el más frecuente de todos:

ajustes.json
{
  "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.

Salida en pantalla
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.

preferencias.py
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:

Salida en pantalla
Al arrancar: claro 14
Tras recargar: oscuro 16
Recientes: ['/Users/erwin/informes año 2026']

Y deja esto en el disco:

preferencias.json
{
  "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ó.