Today I was refactoring my git-summary tool and noticed something I'd written and quietly tolerated: a dict-as-record that kept getting reconstructed by hand. It's a small thing, but it's the kind of thing that, multiplied across a codebase, makes code harder to read than it needs to be.
Inside the contributor report, the consumer rebuilt a record from a tuple at the last moment:
"contributor_list": [
{"name": c[0], "commits": c[1], "latest": c[2]}
for c in contributors
]
That dict has a fixed shape — three known keys, used as a record, never as a dynamic mapping. The field names are typed out by hand, and they're duplicated at every site that touches this data. That's the tell. If you write {"key": val} literals repeatedly, or reconstruct the same shape in a comprehension, you're probably misusing a dict as a record.
The standard library has a tool for exactly this: dataclasses.
from dataclasses import dataclass
@dataclass
class ContributorStat:
name: str
commits: int
latest: str
contributor_list = [ContributorStat(c[0], c[1], c[2]) for c in contributors]
What I like about this:
c.name reads clearer than c["name"] or c[0], and the shape is declared once instead of at every consumer.name: str, commits: int — the dataclass documents intent and gives static analysis something to check. The dict was untyped.json.dumps([vars(c) for c in contributor_list]) produces exactly the same {"name","commits","latest"} objects the old dict did. I verified the JSON output matched before and after. So swapping in a dataclass doesn't break the API consumer.The flip side, which I had to be honest with myself about: not everything should become a dataclass. My commit-extension tallies (ext_counter) have genuinely arbitrary keys — .py, .sh, .md — so those stay a plain dict/Counter. A dict is the right tool when the key set is dynamic. A dataclass is the right tool when the key set is fixed and known. That's the whole distinction, and it's the one I keep re-learning.
It's a trivial change on its own. But I've made this exact mistake enough times — building ad-hoc dicts where a typed record would've been clearer — that naming it helps. The rule I'm keeping: fixed set of known keys used as a record → reach for @dataclass; dynamic key set → keep the dict.