The configuration dictionary in your batch job is a liar. It doesn’t tell you when a key is misspelled, when two call sites disagree on a default, or when a nested structure has silently drifted from what the code expects. It fails quietly, and the failure only surfaces when a routine change lands on the wrong assumption.

Python’s standard library has had a better answer since 3.7, and it costs almost nothing. Decorate a class with @dataclass, annotate the fields, and the dataclasses module generates the initializer, representation, and equality methods for you. But here’s the boundary that shapes every design decision: those annotations describe the model, not enforce it. Pass a string where an int belongs and the dataclass constructs happily. As PEP 557 puts it, the decorator uses annotations to discover fields, and the types are otherwise not examined. It’s a contract you can read, not a validator that enforces itself.

The real work happens in composition. When a config sprawls past twenty fields, the answer isn’t a bigger dataclass — it’s nesting smaller ones. A request object, a dataset record, a hyperparameter block: each gets its own class with a readable shape. But don’t expect ORM-style magic. Pass retry={“max_attempts”: 5} and the dataclass stores the dictionary as-is. Nothing walks the annotations converting nested dictionaries into nested dataclasses for you.

Mutable defaults are where the module makes you slow down deliberately. Write tags: list[str] = [] and Python refuses at class-definition time with a ValueError, rejecting the shared mutable default outright. The default_factory callable communicates actual intent: every instance gets a fresh list built at construction. The same applies to nested records, which is why you’d use field(default_factory=RetryPolicy) rather than a single shared instance that every job would silently co-own.

None of this makes dataclasses a replacement for Pydantic when you need real validation at runtime. But for the config dict that’s been quietly accumulating technical debt, a dataclass turns silent disagreement into an AttributeError at the exact line that’s wrong — and your type checker flags it before the code even runs.