Skip to content
DevOps AI ToolKit
Newsletter
All guides
AI for Bash & Python Automation By James Joyner IV · · 9 min read

Python dataclasses for Modeling Ops Data Cleanly

Stop passing dicts and tuples around your automation. Python dataclasses give your ops scripts typed, self-documenting records with almost no boilerplate.

  • #python
  • #bash
  • #data-modeling

The Python automation scripts I inherit are almost always built on dictionaries. A function returns {"name": ..., "status": ..., "restarts": ...}, the caller reaches into it with string keys, a typo in one of those keys becomes a KeyError three functions away, and nobody knows what shape the data is supposed to be without tracing the whole call chain. It works, until it does not.

dataclasses, in the standard library since Python 3.7, fixes this with almost no ceremony. You get a typed, named record with an auto-generated constructor, repr, and equality — and your data finally documents itself. Here is how I use them in real ops code, and where AI fits in.

From dict soup to a typed record

Compare a dict-based pod record with a dataclass:

# Before: what keys does this have? who knows.
pod = {"name": "api-7f9", "ready": True, "restarts": 0}

# After: the shape is the definition.
from dataclasses import dataclass

@dataclass
class Pod:
    name: str
    ready: bool
    restarts: int

pod = Pod(name="api-7f9", ready=True, restarts=0)

The decorator generates __init__, __repr__, and __eq__ for you. Now pod.restarts is an attribute your editor autocompletes, a typo like pod.restart is an obvious AttributeError at the access point, and print(pod) shows Pod(name='api-7f9', ready=True, restarts=0) instead of a bare dict.

Defaults and computed fields

Fields can have defaults, and __post_init__ lets you compute derived values or validate after construction:

from dataclasses import dataclass, field

@dataclass
class DeployConfig:
    env: str
    replicas: int = 3
    regions: list[str] = field(default_factory=list)

    def __post_init__(self):
        if self.replicas < 1:
            raise ValueError("replicas must be >= 1")

Note field(default_factory=list) for mutable defaults. You cannot write regions: list = [] — every instance would share the same list, a classic Python footgun. default_factory creates a fresh list per instance. The dataclass machinery actually errors if you try the unsafe form, which is a nice guardrail.

Pro Tip: Any mutable default — list, dict, set — must use field(default_factory=...). If an AI assistant hands you tags: list = [], that is a bug; the shared-mutable-default trap is subtle enough that it slips past review constantly.

Frozen dataclasses for config that should not change

For configuration and other values that should be immutable once built, frozen=True makes the instance read-only and hashable (so you can use it in sets and as dict keys):

from dataclasses import dataclass

@dataclass(frozen=True)
class Endpoint:
    host: str
    port: int

e = Endpoint("db.internal", 5432)
# e.port = 5433  -> raises FrozenInstanceError

I default config records to frozen. If something tries to mutate config halfway through a run, I want a loud error, not a silent state change.

Converting to and from dicts and JSON

Ops data crosses boundaries constantly — JSON from an API, a row from a CSV. dataclasses.asdict serializes out, and you construct in with dict unpacking:

import json
from dataclasses import dataclass, asdict

@dataclass
class Pod:
    name: str
    ready: bool
    restarts: int

# dataclass -> JSON
pod = Pod("api-7f9", True, 0)
print(json.dumps(asdict(pod)))

# JSON -> dataclass (when keys match field names)
data = json.loads('{"name": "w-1", "ready": false, "restarts": 4}')
pod = Pod(**data)

The Pod(**data) form is clean but assumes the input keys exactly match your fields. If the source has extra keys or different names, write a small classmethod constructor to map them explicitly rather than letting bad input blow up __init__.

Ordering and comparison

Need to sort a list of records — say, pods by restart count, or hosts by name? order=True generates comparison methods based on field order, so the records become sortable out of the box:

from dataclasses import dataclass, field

@dataclass(order=True)
class Incident:
    severity: int
    title: str = field(compare=False)

incidents = [
    Incident(3, "disk full"),
    Incident(1, "api down"),
    Incident(2, "slow queries"),
]
incidents.sort()  # sorts by severity, then would tie-break on later fields

The field(compare=False) on title keeps it out of the comparison, so two incidents with the same severity are treated as equal regardless of title. This is how you control sort keys precisely without writing a __lt__ by hand.

A real example: parsing a status command

Here is the pattern end to end — taking messy CLI output and turning it into clean records you can filter and report on:

from dataclasses import dataclass
import subprocess, json

@dataclass(frozen=True)
class Node:
    name: str
    ready: bool
    cpu_percent: float

def get_nodes() -> list[Node]:
    out = subprocess.run(
        ["kubectl", "get", "nodes", "-o", "json"],
        capture_output=True, text=True, check=True, timeout=30,
    ).stdout
    data = json.loads(out)
    nodes = []
    for item in data["items"]:
        name = item["metadata"]["name"]
        ready = any(
            c["type"] == "Ready" and c["status"] == "True"
            for c in item["status"]["conditions"]
        )
        nodes.append(Node(name=name, ready=ready, cpu_percent=0.0))
    return nodes

unhealthy = [n for n in get_nodes() if not n.ready]

The for c in ... mapping is explicit on purpose: I do not unpack arbitrary API JSON straight into a dataclass, because the source can change shape. I pull out the fields I need by hand, which makes the failure mode a clear KeyError at parse time rather than a corrupt record deep in the program.

When to reach for pydantic instead

Dataclasses do not validate types at runtime — Pod(name=123, ...) happily stores an int in a str field. That is fine when you control the data. When the data comes from an untrusted source (a webhook, a user-supplied config, an external API), you want real validation and coercion, and that is pydantic’s job, not dataclasses’. The rule I use: dataclasses for internal records I create, pydantic for data crossing a trust boundary. Knowing which one a given record needs is the actual design decision.

Letting AI generate the models

Turning a sample JSON response or a pile of dict-handling code into clean dataclasses is mechanical, and an AI assistant does it quickly. I will paste a sample API payload into Claude or Cursor and ask for matching dataclasses with correct types and sensible defaults.

It is a fast junior engineer for this, and I review every model before it goes into production code. Specifically I check:

  • That no mutable default slipped in as a bare [] or {} instead of field(default_factory=...).
  • That config records that should be immutable are marked frozen=True.
  • That the AI did not silently assume runtime type validation a dataclass does not provide — if the data crosses a trust boundary, I switch it to pydantic.
  • That no real secrets were pasted into the prompt. A sample payload for modeling should have fake tokens; the AI never needs your real ones.

I keep that checklist in the prompt workspace and route model-heavy changes through the code review dashboard before they ship.

Conclusion

Dataclasses turn anonymous dict soup into typed, self-documenting records for almost no cost: @dataclass for the basics, field(default_factory=...) for mutable defaults, frozen=True for immutable config, and asdict/**unpack to cross JSON boundaries. Use them for the internal data you control and pydantic for anything from an untrusted source. Let an AI generate the models from sample payloads, then review for the mutable-default trap and the trust-boundary decision the assistant cannot make for you.

More in the Bash and Python automation category. Reusable starters are in the prompt library, and curated sets are in the prompt packs.

Free download · 368-page PDF

Download the Free 500-Prompt DevOps AI Toolkit

500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.

  • 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
  • Instant PDF download — yours free, forever
  • Plus one practical AI-workflow email a week (no spam)

Single opt-in · unsubscribe anytime · no spam.