We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Data Modelling and Invariants step 19 of 25
NamedTuple: when a record should be a tuple
A cache keyed on (user_id, tenant_id) collides with an (x, y) coordinate, and
no test finds it. That is not a hypothetical: it is the direct consequence of the
one property that makes NamedTuple different from every other record type in
Python.
Cross-type equality is the whole trade
class Point(NamedTuple):
x: int
y: int
class Date(NamedTuple):
x: int
y: int
Point(1, 2) == Date(1, 2) # True
Point(1, 2) == (1, 2) # True
A dataclass returns False for both, because its generated __eq__ starts with
other.__class__ is self.__class__. A NamedTuple inherits tuple.__eq__,
which compares contents only. There is no class check anywhere in the chain.
That is not a bug — it is the contract you asked for. A NamedTuple is a
tuple: it unpacks, it slices, it concatenates, it goes straight into %
formatting and zip, and it compares equal to any tuple with the same elements.
When the value genuinely is a pair or a row — a coordinate, an RGB triple, a
DB row, a divmod result — that compatibility is the point, and the type is the
right one.
When the value is a domain entity — an Order, a User, a CacheKey — it is
the wrong one, for exactly the same reason.
The rest of the trade-offs
-
Attribute access is slower, not faster: ~8.3 ns through the
_tuplegetterdescriptor vs ~3.8 ns for a slotted dataclass. It is a tuple index plus a descriptor call. - Equality is much faster: ~9.9 ns vs ~45.8 ns for a dataclass, because it is one C-level tuple comparison instead of a Python-level field walk.
-
Immutable, hashable, ordered for free.
_replace()is thereplace()equivalent;_asdict()returns a plain dict withoutasdict‘s deep-copy tax. -
⚠ 3.14 breaking change:
super()(zero-arg) or__class__inside a method of aNamedTuplesubclass now raisesTypeError. If you have such a method, it worked on 3.13 and does not on 3.14.
The strict-mode reason to prefer typing.NamedTuple
collections.namedtuple("Point", "x y") produces a class whose attributes are
typed Any. p.x + "oops" type-checks. --strict reports nothing — the
only flag that catches it is the non-strict --disallow-any-expr.
typing.NamedTuple types every field correctly. So the choice between the two
spellings is not style: one of them silently punches an Any-shaped hole in your
module.
Your task
Complete Interval(NamedTuple) with lo: int and hi: int:
-
overlaps(other)— true when the two intervals share at least one point, including touching at an endpoint ([1,2]overlaps[2,5]); -
merge(other)— the smallest interval containing both; -
__contains__(point)—point in intervalis true for anintinside the closed range, and false (never an exception) for anything else. Note that this overridestuple.__contains__, which would otherwise test membership among the elements.
Then same_kind(a, b) — true only when the two tuples are the same concrete
class — and merge_all(intervals) returning the coalesced, ascending list.
solve(intervals, probe) returns:
-
"merged"— the merged intervals, asIntervalobjects (they serialise as tuples; returning lists fails); -
"contains"—probe in intervalfor each merged interval; -
"tuple_equality"—Interval(1, 2) == (1, 2); -
"cross_type_equality"—Interval(1, 2) == Span(1, 2), asked throughoperator.eq(written literally it is a mypycomparison-overlaperror —--strict-equalityis, pleasingly, the one tool that does catch this hole, and only when both sides are statically known); -
"same_kind"—same_kind(Interval(1, 2), Span(1, 2)).
The last three document the contract rather than test your cleverness: two of
them are True, and the only way to tell a Span from an Interval is
type(a) is type(b). Write that helper deliberately — in real code it is the
thing you reach for when a NamedTuple key needs to not collide.
No zero-argument super() and no __class__ anywhere inside these classes.
Stuck?
Python reference solution
Sign in to attempt this problem and reveal the reference solution.