# JSON en Python: leer y escribir archivos, CSV, pathlib y codificación UTF-8

URL: https://skillaria.top/es/guides/python/json-python

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. Use `Path(__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 pase `default`.
- `KeyError` después de cargar JSON: las claves numéricas se convirtieron en cadenas.
- Un archivo propio se llama `json.py` o `csv.py`: tapa el módulo estándar y `import json` importa 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.

## FAQ

**¿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.


---
Source: Skillaria.Top (https://skillaria.top)
