We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Data Modelling and Invariants step 17 of 25
kw_only, KW_ONLY and the inheritance ordering wall
Insert one field into a shared base class and every positional call site in the
codebase now binds arguments to different fields. If the types happen to line up
— two strs, an int and a bool — there is no type error and no runtime
error, just wrong data, written to your database, forever.
That is the real reason to make record types keyword-only. The ordering rule is the mechanism that forces the issue.
The wall
A generated __init__ is an ordinary function signature, so it obeys the
ordinary rule: a parameter without a default cannot follow one with a default.
Applied across inheritance, that becomes a permanent constraint:
@dataclass
class Resource:
id: str
created_at: str = "1970-01-01T00:00:00Z"
@dataclass
class Bucket(Resource):
name: str # TypeError: non-default argument 'name' follows default argument
Once a base class has one defaulted field, no subclass may ever add a
required one. In a shared Resource/Entity/BaseModel base this shows up
about three months in, and the usual “fix” is the worst one available:
name: str | None = None plus a runtime if self.name is None: raise. You have
now moved a compile-time guarantee into a runtime check, and mypy will make you
handle None at every single use site forever.
The two real fixes
kw_only=True on the decorator: every field becomes keyword-only, so
__init__ has no positional parameters at all, so the ordering rule cannot bite.
Fields still have an order (reverse MRO, as always) — it just no longer
constrains anything.
The KW_ONLY sentinel, for finer control:
@dataclass
class Point:
x: float
y: float
_: KW_ONLY
label: str = ""
Everything after the marker is keyword-only. One per class; its name is
ignored (_ by convention); it is not a field.
The second-order effect nobody mentions
Keyword-only fields are excluded from __match_args__. So a fully keyword-only
dataclass has __match_args__ == (), and structural pattern matching sees no
positional prefix at all:
match bucket:
case Bucket(bucket_id): # TypeError at runtime: no positional sub-patterns
...
case Bucket(name=n): # fine — keyword patterns still work
...
That is a real trade, not a bug: you gave up positional construction, and positional matching goes with it. Keyword patterns are the better habit anyway, for exactly the same reason keyword construction is.
Your task
The module defines a three-level hierarchy: Resource (with id, a defaulted
created_at, and a required owner declared after it), Bucket(Resource)
adding a required name, and VersionedBucket(Bucket) adding a required
version. As written it does not even import.
Make it work — without reordering fields, without giving anything a default it
should not have, and without the | None = None + runtime-check workaround. Then
implement:
def solve(target, resource_id, owner, name="", version=0) -> dict[str, object]:
returning:
-
"field_order"—[f.name for f in fields(obj)], in the documented reverse-MRO order; -
"match_args"—list(type(obj).__match_args__); -
"repr"—repr(obj); -
"positional_error"—"TypeError"if constructing the class with a single positional argument fails,""otherwise (the helper is written for you).
Types. mypy catches this class of bug statically in both directions: the
ordering violation is “Attributes without a default cannot follow attributes with
one”, and a positional call to a keyword-only dataclass is “Too many positional
arguments”. That second one is why _positional_error has to launder the class
through a Callable[..., object] to attempt the call at all.
Stuck?
Python reference solution
Sign in to attempt this problem and reveal the reference solution.