Saltar al contenido principal

Salida estructurada en Python

Metadatos de la lección

Autoría:
Ignacio Slater-Muñoz
Última actualización:
28 de marzo de 2026

Cambios recientes:

  • f1fc94f · 28 de marzo de 2026 · 📚 feat(bibliography): add Python structured-output catalog references ( GitLab / GitHub )
  • 8880728 · 27 de marzo de 2026 · ✨ feat(notes): add abstract slots and Python structured-output lesson ( GitLab / GitHub )

Encuentra el código de la lección:

Abstract

Si en la lección base PowerShell mostraba cómo producir objetos listos para consola y pipeline, aquí cambia el criterio de diseño de la salida estructurada. En Python, suele pensarse menos como algo que el shell presenta automáticamente y más como un dato explícito que luego otra parte del programa, un archivo o una API puede consumir.

Por eso esta comparación se apoya en dos movimientos frecuentes: serializar datos a JSON para intercambiarlos con otras herramientas y usar @dataclass cuando esa información deja de ser ocasional y conviene modelarla como parte explícita de la interfaz del programa.

De Python a JSON

En la comparación con PowerShell aparece una diferencia importante de foco. En PowerShell, la salida estructurada suele pensarse para consumo inmediato: verla en consola, filtrarla, proyectarla o encadenarla en el pipeline. En Python, en cambio, muchas veces la estructura se prepara para consumo externo: guardarla en un archivo, enviarla por una API o compartirla con otro sistema.

JSON encaja bien en ese escenario porque ofrece un formato textual simple, portable y fácil de leer desde muchos lenguajes. Por eso es común construir una estructura en memoria y luego convertirla en una representación JSON estable.

Guardar una estructura Python como JSON
scripts-py/structured-output/json_utils.py
from __future__ import annotations

import json
from pathlib import Path
from typing import TypeAlias

JSONValue: TypeAlias = (
    dict[str, "JSONValue"]
    | list["JSONValue"]
    | str
    | int
    | float
    | bool
    | None
)

def save_to_json(data: JSONValue, filename: str | Path) -> None:
    with Path(filename).open("w", encoding="utf-8") as file:
        json.dump(data, file, indent=4, ensure_ascii=False, sort_keys=True)
Aquí la estructura ya no está pensada solo para inspección inmediata, sino también para persistencia e intercambio con otros programas.

Detalles clave

  • TypeAlias permite dar nombre a un tipo complejo. En este caso, JSONValue describe los valores que pueden aparecer en un JSON: objetos, listas, strings, números, booleanos y None.
  • El alias es recursivo porque un objeto JSON puede contener otros objetos JSON o listas de valores JSON dentro de sí. Por eso el tipo se define en términos de sí mismo en dict[str, "JSONValue"] y list["JSONValue"] (nota que el tipo va entre comillas ya que se refiere a un tipo que aún no está completamente definido a esas alturas).
  • json.dump escribe la estructura directamente en un archivo abierto. No devuelve un string: toma el dato Python y lo serializa en el flujo de salida.
  • Las opciones elegidas controlan el formato del archivo: indent=4 agrega sangría para hacerlo legible, ensure_ascii=False preserva caracteres no ASCII sin escapar innecesariamente, y sort_keys=True ordena las claves para obtener una salida más estable y fácil de comparar.

De JSON a @dataclass y viceversa

En la lección base, [PSCustomObject] marcaba el momento en que la salida dejaba de ser solo un contenedor clave-valor y pasaba a tener una forma más explícita.

Python no ofrece un equivalente directo con la misma integración de consola. Aun así, @dataclass suele marcar un cambio parecido: dejamos de usar un contenedor genérico y pasamos a modelar una estructura con significado más claro y más estable.

Ese cambio se vuelve útil cuando la forma del dato deja de ser accidental: conviene nombrarla, acceder a sus campos como atributos y tratarla como parte de una interfaz que otras partes del programa pueden asumir y reutilizar.

Convertir entre JSON y una estructura explícita
scripts-py/structured-output/comics.py
from dataclasses import dataclass
import json
from pathlib import Path
from json_utils import JSONValue

@dataclass(frozen=True)
class Comic:
    title: str
    writer: str
    release_year: int

    def to_json_serializable(self) -> JSONValue:
        return self.__dict__

def comic_from_json(file_path: str | Path) -> Comic:
    with Path(file_path).open("r", encoding="utf-8") as file:
        data = json.load(file)
        return Comic(**data)
Aquí la ganancia principal no es una tabla automática, sino un modelo más claro y más estable como contrato para quien lee y mantiene el código.

Además de volver más explícita la intención del modelo, @dataclass genera automáticamente un constructor y una representación textual útil para depuración. La diferencia importante no es solo cómo se imprime el valor, sino que ahora la estructura tiene nombre y puede formar parte explícita de la interfaz del programa.

Detalles clave

  • @dataclass genera una clase pensada para representar datos. En este caso, deja explícito que un Comic tiene título, escritor y año de publicación.
  • Con frozen=True, la instancia se vuelve esencialmente inmutable: una vez creada, no deberías cambiar sus campos. Para este tipo de modelo eso suele ser útil porque reduce cambios accidentales y hace más claro su uso.
  • El método to_json_serializable devuelve self.__dict__, es decir, el diccionario de atributos de la instancia. Aquí funciona bien porque todos los campos ya son valores simples serializables a JSON. En un caso real, si hubiera fechas, enums u otros objetos más complejos, habría que convertirlos explícitamente antes de serializar.
  • json.load hace la operación inversa a json.dump: lee el contenido del archivo y lo convierte en una estructura Python, en este caso un dict.
  • Luego, Comic(**data) usa unpacking de diccionario para mapear cada clave del JSON a un parámetro del constructor. Esto supone que el archivo tiene exactamente la forma esperada. Aquí no hay validación explícita: el ejemplo asume que el JSON ya viene bien formado y con las claves correctas.

Uso

Para probarlo, se puede usar el siguiente JSON de ejemplo, guardado en un archivo llamado comic.json:
JSON logo
{
    "title": "Nightwing: Time of the Titans",
    "writer": "Tom King",
    "release_year": 2024
}
y luego ejecutarlo desde la consola de Python o desde otro script:
comic = comic_from_json("comic.json")
print(comic)

Conclusiones

La idea central de esta lección no es que Python replique el modelo de salida de PowerShell, sino que organiza la salida estructurada con otras prioridades. Mientras PowerShell destaca cuando quieres objetos listos para consola y pipeline, Python suele sobresalir cuando necesitas datos explícitos que luego persistes, intercambias o entregas a otras partes del programa.

En ese recorrido, JSON funciona como uno de los puentes más visibles: una estructura en memoria puede guardarse en un archivo, viajar hacia otra herramienta y volver a levantarse como dato dentro de Python. Cuando esa estructura deja de ser un diccionario ocasional y pasa a tener una forma conocida, @dataclass ayuda a nombrarla y tratarla como un modelo más claro y reusable.

La lección propone, entonces, otro criterio de diseño para la salida estructurada: en Python, muchas veces importa más que el dato sea fácil de intercambiar, serializar y reutilizar que presentarlo automáticamente en terminal. La decisión importante no es solo cómo mostrar la salida, sino cuándo basta un contenedor flexible y cuándo conviene declarar un modelo explícito.

Puntos clave

  • En Python, la salida estructurada suele pensarse menos para inspección inmediata en consola y más para persistencia, interoperabilidad e intercambio con otros sistemas.
  • JSON ofrece un formato textual portable y estable para mover estructuras Python fuera del programa.
  • Un dict sirve bien como contenedor flexible, pero se vuelve un contrato débil cuando la forma del dato ya debería ser conocida y estable.
  • @dataclass permite nombrar esa estructura, acceder a sus campos como atributos y modelarla como parte explícita de la interfaz del programa.
  • Convertir entre JSON y un modelo explícito hace más claro qué datos espera y produce el programa, aunque en ejemplos simples todavía no haya validación formal.

¿Qué nos llevamos?

Más que imitar la ergonomía de PowerShell, Python invita a pensar la salida estructurada como dato interoperable. Una buena regla práctica es esta: usa un contenedor flexible cuando la estructura sea transitoria, pero da el paso hacia un modelo explícito cuando esa forma ya empiece a comportarse como parte estable de tu programa.

¿Con ganas de más?