Leer y escribir JSON en Python

El módulo json viene con Python y hace lo evidente. Lo que merece la pena saber son los dos argumentos que casi nadie pone y los tipos que se niega a convertir.

Lo básico

import json

texto  = json.dumps(datos, ensure_ascii=False, indent=2)   # objeto -> texto
vuelta = json.loads(texto)                                 # texto  -> objeto

with p.open("w", encoding="utf-8") as f: json.dump(datos, f)    # directo a archivo
with p.open(encoding="utf-8") as f:      datos = json.load(f)   # y desde archivo

Los cuatro nombres se confunden siempre: los que llevan una s trabajan con cadenas (string), los que no, con archivos.

Los dos argumentos que hay que poner

sin y con ensure_ascii=False{"ciudad": "A Coru\u00f1a"}
{"ciudad": "A Coruña"}

ensure_ascii=False deja los acentos como están. JSON es UTF-8 por definición, así que escaparlos solo sirve para hacer el archivo ilegible. Y indent=2 si lo va a leer una persona; sin él sale todo en una línea.

Python JSON
dict objeto
list, tuple array — las tuplas vuelven como listas
str string
int, float number
True / False true / false
None null

Esa fila de las tuplas importa: una tupla sobrevive a la ida pero no a la vuelta. Lo mismo pasa con las claves numéricas de un diccionario, que vuelven como cadenas.

Lo que no sabe convertir

salidadate      -> Object of type date is not JSON serializable
Decimal   -> Object of type Decimal is not JSON serializable
set       -> Object of type set is not JSON serializable
bytes     -> Object of type bytes is not JSON serializable

Se resuelve con default=, una función a la que json recurre cuando no sabe qué hacer:

def convertir(o):
    if isinstance(o, (date, datetime)): return o.isoformat()
    if isinstance(o, Decimal): return str(o)
    if isinstance(o, set): return sorted(o)
    raise TypeError(f"no sé convertir {type(o).__name__}")

json.dumps(datos, default=convertir, ensure_ascii=False)
salida{"fecha": "2026-10-07", "precio": "1.5", "tags": ["a", "b"]}
El Decimal va como cadena a propósito. Convertirlo a float reintroduce el error de coma flotante que estabas evitando al usar Decimal. Con texto, al leerlo haces Decimal(valor) y recuperas el número exacto.

Con dataclasses

from dataclasses import dataclass, asdict

@dataclass
class Producto:
    nombre: str
    precio: float

json.dumps(asdict(Producto("gorra", 12.5)))      # al JSON
Producto(**json.loads(crudo))                     # y de vuelta
salida{"nombre": "gorra", "precio": 12.5}
Producto(nombre='gorra', precio=12.5)

Ese Producto(**...) funciona mientras las claves coincidan con los campos. Si el JSON trae campos de más, revienta; si quieres algo que aguante datos ajenos, lo habitual es Pydantic, que además valida los tipos.

Cuando el JSON está roto

salidaJSONDecodeError: Illegal trailing comma before end of object (línea 1, columna 8)

JSONDecodeError hereda de ValueError y trae .msg, .lineno, .colno y .pos. Con un archivo grande, esos números son la diferencia entre arreglarlo en un minuto y buscar a ojo.

Y una advertencia práctica: una coma de más al final es inválida en JSON, aunque Python la acepte en sus propios diccionarios y casi todos los editores la pinten en verde. Tampoco se admiten comentarios.

Todo el código se ejecutó con Python 3.14 en un contenedor limpio antes de publicar esta página; las salidas están copiadas de esa ejecución.