Batu Lab NotesPractical developer guides

Use kw_only dataclass fields to prevent argument swaps

By Batu · English technical notes

Also published in our Blogger archive.

Use kw_only dataclass fields to prevent argument swaps

Positional arguments are compact, but they can hide a swap when adjacent values share a type. An endpoint’s port and timeout are both integers, so Endpoint("api", 5, 443) looks plausible while carrying the wrong meaning. Marking those dataclass fields with field(kw_only=True) makes the generated initializer require their names. The call site therefore records whether 443 is port and whether 5 is timeout_seconds.

The valid construction is asserted and printed. The second construction supplies the two integer values positionally; it raises TypeError, which the example catches to demonstrate the generated signature’s runtime enforcement. This feature prevents accidental positional swapping at that constructor boundary, not every possible configuration mistake. A caller can still deliberately attach an unsuitable number to a correct keyword, so domain validation remains separate. Keyword-only fields are also omitted from the dataclass’s positional pattern-matching arguments, which may affect matching code. Per-field kw_only and the decorator option were added in Python 3.10.

See the official dataclasses.field documentation. AI assistance disclosure: this article was drafted with AI assistance and checked using a synthetic example.

from dataclasses import dataclass, field


@dataclass(frozen=True)
class Endpoint:
    host: str
    port: int = field(kw_only=True)
    timeout_seconds: int = field(kw_only=True)


endpoint = Endpoint("api", port=443, timeout_seconds=5)
assert endpoint == Endpoint("api", port=443, timeout_seconds=5)
try:
    Endpoint("api", 443, 5)
except TypeError:
    print("positional values rejected")
else:
    raise AssertionError("keyword-only fields accepted positional values")
print(f"{endpoint.host}:{endpoint.port} timeout={endpoint.timeout_seconds}")
positional values rejected
api:443 timeout=5