Skip to content

📙 Clase 20 — JSON: guardar datos estructurados

Fase 4 · Archivos y el mundo exterior ⬅️ Volver al índice de clases

🎯 Qué aprendí

  • JSON: el formato universal para guardar/intercambiar datos con estructura.
  • json.dump/json.load (a archivo) y json.dumps/json.loads (a texto).
  • El hito del curso: mi To-Do por fin recuerda las tareas al cerrar. 🎉

📖 PARTE TEÓRICA

🌐 1. ¿Qué es JSON?

JSON (JavaScript Object Notation) es texto con una estructura que casi calca la de Python. Es EL formato de configuraciones, APIs y persistencia ligera.

PythonJSON
dict {"a": 1}{"a": 1}
list [1, 2][1, 2]
str "hola""hola" (solo comillas dobles)
int/floatnúmero
True / Falsetrue / false (minúscula)
Nonenull

⚠️ Diferencias que delatan a un JSON: comillas dobles obligatorias, true/false/null en minúscula, y no admite comentarios ni comas finales.

💾 2. Guardar y cargar (archivo): dump / load

Tu "tabla en memoria" (lista de dicts, Clase 04) viaja al disco en dos líneas:

python
import json

tareas = [
    {"texto": "Comprar pan", "hecha": False},
    {"texto": "Estudiar Python", "hecha": True},
]

# GUARDAR: dump = "volcar" al archivo
with open("tareas.json", "w", encoding="utf-8") as f:
    json.dump(tareas, f, ensure_ascii=False, indent=2)

# CARGAR: load = leer y reconstruir las estructuras
with open("tareas.json", encoding="utf-8") as f:
    cargadas = json.load(f)

print(cargadas == tareas)              # True  ← ida y vuelta perfecta
print(type(cargadas).__name__)         # list  (¡vuelve como lista de dicts!)

El archivo generado (mírala: es tu estructura, legible):

json
[
  {
    "texto": "Comprar pan",
    "hecha": false
  },
  {
    "texto": "Estudiar Python",
    "hecha": true
  }
]

📌 Los dos parámetros que siempre querrás: ensure_ascii=False (guarda "ñ/á" tal cual, no ñ) e indent=2 (formato legible para humanos).

🔤 3. A texto y de vuelta: dumps / loads

La versión con s (string) no toca archivos — convierte a/desde texto. La usarás con APIs:

python
s = json.dumps({"nombre": "Ana", "edad": 30}, ensure_ascii=False)
print(s)                  # {"nombre": "Ana", "edad": 30}   ← un string
print(json.loads(s))      # {'nombre': 'Ana', 'edad': 30}   ← de vuelta a dict

🧪 Tip de entrevista: "¿dump vs dumps?" → Sin s: escribe a archivo. Con s: devuelve string. Igual para load/loads al leer.


🖥️ EN TU APP DE ESCRITORIO — el reto del Ejercicio 2, resuelto

Este es el "reto extra" de tu To-Do: que las tareas no se pierdan al cerrar. El patrón completo cargar/guardar con JSON:

python
import json

class App(ctk.CTk):
    ARCHIVO = "tareas.json"

    def __init__(self):
        super().__init__()
        self._tareas = self._cargar()          # al abrir: restaurar

    def _cargar(self):
        try:
            with open(self.ARCHIVO, encoding="utf-8") as f:
                return json.load(f)            # lista de dicts, lista p/ usar
        except FileNotFoundError:
            return []                          # primer arranque
        except json.JSONDecodeError:
            return []                          # archivo corrupto: no morir

    def _guardar(self):
        with open(self.ARCHIVO, "w", encoding="utf-8") as f:
            json.dump(self._tareas, f, ensure_ascii=False, indent=2)

    def _agregar(self):
        self._tareas.append({"texto": self.entry.get(), "hecha": False})
        self._guardar()                        # persistir en cada cambio
        self._pintar()

¿Y si tu modelo son objetos Tarea (Fase 3)? Conviertes al guardar y al cargar:

python
def _guardar(self):
    datos = [{"texto": t.texto, "hecha": t.hecha} for t in self._tareas]
    ...
def _cargar(self):
    ...
    return [Tarea(d["texto"], d["hecha"]) for d in json.load(f)]

⚠️ json solo serializa tipos básicos (dict/list/str/num/bool/None). Un objeto Tarea directo lanza TypeError: Object of type Tarea is not JSON serializable — por eso el paso objeto → dict. (¿Notas el patrón? Es el a_dict() de la Clase 13.)

Otro uso inmediato: configuración de tu app (tema, tamaño, idioma) en un config.json.


🗄️ CON BASE DE DATOS (caso de uso)

JSON es tu peldaño previo a SQLite — y su compañero después:

JSONSQLite (Fase 6)
Ideal paraconfig, listas pequeñas (< miles)muchos registros, búsquedas, filtros
Buscar/filtrarcargas TODO y filtras en Pythonla BD filtra por ti (WHERE)
Modificar un registroreescribes el archivo enteroUPDATE ... WHERE id=?

🗄️ Además, las APIs hablan JSON: cuando en la Clase 23 consumas una API, la respuesta será JSON que requests convierte a dicts — todo esto se reusa.


🏋️ EJERCICIOS CON SOLUCIÓN

Ejercicio 1 — Config de la app

Guarda {"tema": "oscuro", "fuente": 14} en config.json y vuelve a cargarlo.

Ver solución
python
import json

config = {"tema": "oscuro", "fuente": 14}
with open("config.json", "w", encoding="utf-8") as f:
    json.dump(config, f, ensure_ascii=False, indent=2)

with open("config.json", encoding="utf-8") as f:
    cargada = json.load(f)
print(cargada["tema"])    # oscuro

Ejercicio 2 — Carga a prueba de fallos

Escribe cargar_lista(ruta) que devuelva la lista del JSON, o [] si el archivo no existe o está corrupto.

Ver solución
python
import json

def cargar_lista(ruta):
    try:
        with open(ruta, encoding="utf-8") as f:
            return json.load(f)
    except (FileNotFoundError, json.JSONDecodeError):
        return []

print(cargar_lista("tareas.json"))   # la lista guardada
print(cargar_lista("nada.json"))     # []

Ejercicio 3 — Objeto ↔ JSON

Con class Nota: __init__(self, titulo, texto), guarda una lista de notas en JSON y recárgala como objetos.

Ver solución
python
import json

class Nota:
    def __init__(self, titulo, texto):
        self.titulo = titulo
        self.texto = texto

notas = [Nota("Compra", "pan y leche"), Nota("Idea", "app de gastos")]

with open("notas.json", "w", encoding="utf-8") as f:
    json.dump([{"titulo": n.titulo, "texto": n.texto} for n in notas],
              f, ensure_ascii=False, indent=2)

with open("notas.json", encoding="utf-8") as f:
    recuperadas = [Nota(d["titulo"], d["texto"]) for d in json.load(f)]
print(recuperadas[0].titulo)    # Compra

❓ Preguntas y respuestas (autoevaluación)

1. ¿Qué estructura de Python calca casi 1:1 un JSON?

Dicts y listas anidados (con str/num/bool/None como valores).

2. ¿json.dump vs json.dumps?

dump escribe a un archivo; dumps devuelve un string.

3. ¿Para qué sirven ensure_ascii=False e indent=2?

Guardar acentos/ñ legibles (no ñ) y formatear el archivo para humanos.

4. ¿Qué pasa si le pasas un objeto Tarea directo a json.dump?

TypeError: not JSON serializable. Hay que convertirlo antes a dict (y al cargar, de dict a objeto).


📎 Apuntes relacionados

➡️ Siguiente

Clase 21 · CSV — exportar tus datos a Excel.