2025, Oct 07 19:00
Stop magic numbers: compute __len__ from dataclass Timer fields with dataclasses.fields and keep __iter__ in sync
Make Python dataclasses count timers reliably: derive __len__ from __iter__ via dataclasses.fields, avoid magic numbers, use default_factory for Timer.
Counting items in a dataclass via len should never rely on a magic number. If a class instance owns a set of Timer objects and you already expose them through iteration, the natural next step is to compute the size programmatically. Below is a concise pattern that keeps __len__ in sync with __iter__ without maintenance debt.
Problem
You have a dataclass that aggregates multiple Timer instances. Different subclasses can declare different sets of timers, and you want len(instance) to reflect how many timers are present in that instance. The initial approach hardcodes a magic number, which is brittle and easy to forget when fields change.
from dataclasses import dataclass
@dataclass
class CoreTimers:
    def tick(self):
        pass
    def __iter__(self):
        return iter([])
@dataclass
class DeviceTimers(CoreTimers):
    cycleTimer        = tmr.Timer(0)
    vProtectTimer     = tmr.Timer(0)
    aRestTimer        = tmr.Timer(0)
    aMuteTimer        = tmr.Timer(0)
    vRestTimer        = tmr.Timer(0)
    vMuteTimer        = tmr.Timer(0)
    def tick(self):
        self.cycleTimer.tic()
        self.vProtectTimer.tic()
        self.aRestTimer.tic()
        self.aMuteTimer.tic()
        self.vRestTimer.tic()
        self.vMuteTimer.tic()
    def __len__(self):
        return 6
    def __iter__(self):
        return iter([
            self.cycleTimer,
            self.vProtectTimer,
            self.aRestTimer,
            self.aMuteTimer,
            self.vRestTimer,
            self.vMuteTimer,
        ])
What’s actually going on
Dataclasses are just regular Python classes with a decorator that wires up init and other conveniences. That means __iter__ and __len__ work exactly as in any class. The issue above is that the number of timers is encoded as a literal in __len__, which quickly drifts when fields are added or removed. There is another subtlety: creating Timer objects as class attributes means all instances of the dataclass share the same Timer objects. If the intent is to give each instance its own independent timers, you need a per-instance factory.
Because dataclasses expose declared fields through dataclasses.fields, you can introspect the instance, discover all attributes that represent timers, and derive both iteration and length from a single source of truth. Using a naming convention like the Timer suffix keeps the selection simple and explicit.
Solution
Push the instrumentation to the base class. Use dataclasses.fields for introspection, filter by the Timer suffix, and implement __iter__, __len__, and the bulk tick in terms of that single field source. For independent instances, use default_factory so each dataclass instance gets fresh Timer objects.
from dataclasses import dataclass, fields, field
@dataclass
class CoreTimers:
    @property
    def _timer_names(self):
        for f in fields(self):
            if f.name.endswith("Timer"):
                yield f.name
    def __iter__(self):
        return (getattr(self, name) for name in self._timer_names)
    def __len__(self):
        return len(list(self._timer_names))
    def tick(self):
        for name in self._timer_names:
            getattr(self, name).tic()
@dataclass
class DeviceTimers(CoreTimers):
    cycleTimer: tmr.Timer      = field(default_factory=lambda: tmr.Timer(0))
    vProtectTimer: tmr.Timer   = field(default_factory=lambda: tmr.Timer(0))
    aRestTimer: tmr.Timer      = field(default_factory=lambda: tmr.Timer(0))
    aMuteTimer: tmr.Timer      = field(default_factory=lambda: tmr.Timer(0))
    vRestTimer: tmr.Timer      = field(default_factory=lambda: tmr.Timer(0))
    vMuteTimer: tmr.Timer      = field(default_factory=lambda: tmr.Timer(0))
This design derives iteration and counting from the same definition, so len(instance) and list(instance) always agree, regardless of how many Timer fields the subclass declares. If your real-world timers don’t share a common naming pattern, you can adjust the filter inside _timer_names to match whatever criterion makes sense for your case.
Why this matters
Relying on dataclasses.fields removes guesswork and hardcoded values from collection-like classes. It avoids subtle bugs from shared state by ensuring each dataclass instance gets its own Timer objects. It also concentrates logic in one place: when the set of timers changes, the behavior of __iter__, __len__, and bulk operations stays correct without edits scattered across the class.
Takeaway
Let the class describe itself. With dataclasses, introspection is built in, so you can compute len programmatically based on declared fields instead of hardcoding counts. Use a simple naming convention like the Timer suffix, wire __iter__ and __len__ to that single source, and construct timers with default_factory to keep instances isolated. That’s enough to make your timers collection predictable, maintainable, and free of magic numbers.