Hedronite · Dev Lesson · Polyglot-Dev / Python · Sat 2026-09-19

Python dataclass order and field(compare=False) — priority rows

Sort by the field that decides evaluation. Leave the rest out of the comparison.

Lesson Class: Dev (Python language depth)
Language Focus: @dataclass(order=True) + field(compare=False)
Paired Ops: Azure VNet/NSG association census
Paired Cert: AZ-900 NSG priority first-match
Grounding: Fluent Python Ch.5 pp.178–182
order=True
Generates rich comparisons from fields.
Declaration
Field order is sort-key order.
compare=False
Display fields stay off the scale.
Priority first. Ports are passengers.

<!-- hal:authoritative:yaml -->

Sort by the field that decides evaluation. Leave the rest out of the comparison.

§I — Frame

Ops asks for NSG rules in ascending priority. Dev builds the row type that sorts that way with the stdlib dataclasses module. Stay in pure language depth. Leave Protocol plugins (09-10), TypedDict partial shapes (09-07), and deepcopy (09-04) on their shelves.

Ramalho, printed p.179, lists the two knobs you actually change from the defaults: frozen=True and order=True. Today opens order=True, then tightens it with field(compare=False) so a destination port string never decides which rule comes first.

§II — Language idiom: generated comparisons are field-order comparisons

from dataclasses import dataclass, field


@dataclass(order=True)
class RuleRow:
    priority: int
    name: str
    direction: str
    access: str
    protocol: str
    dest_port: str

order=True generates __lt__, __le__, __gt__, __ge__. Those methods compare the tuple of field values in declaration order. With the class above, sorted(rows) works, and so does row_a < row_b.

That is almost what Ops wants. It fails when two rules share a priority (Azure forbids that inside one NSG direction, but a census merge across NSGs can collide) or when you accidentally declare dest_port before priority. Declaration order is the sort key. Put the deciding field first.

Ramalho's Table 5-3 (printed p.182) adds the field-level escape: compare=False drops a field from __eq__ and from the ordering methods.

@dataclass(order=True)
class RuleRow:
    priority: int
    name: str = field(compare=False)
    direction: str = field(compare=False)
    access: str = field(compare=False)
    protocol: str = field(compare=False)
    dest_port: str = field(compare=False)

Now sorted(rows) orders by priority alone. Display still shows name and port. Equality ignores the display fields too: two rows with the same priority compare equal even when names differ. That is correct for ordering and wrong for identity of rules. If you need both behaviors, keep two types, or compare explicitly with attrgetter("priority") at the call site and leave order=False.

§III — Worked example against the census row

from dataclasses import dataclass, field


@dataclass(order=True, frozen=True)
class RuleRow:
    sort_index: int
    priority: int = field(compare=False)
    name: str = field(compare=False)
    direction: str = field(compare=False)
    access: str = field(compare=False)
    protocol: str = field(compare=False)
    dest_port: str = field(compare=False)

    @staticmethod
    def from_sdk(priority: int, name: str, direction: str, access: str, protocol: str, dest_port: str):
        return RuleRow(priority, priority, name, direction, access, protocol, dest_port)


rows = [
    RuleRow.from_sdk(400, "allow-https", "Inbound", "Allow", "Tcp", "443"),
    RuleRow.from_sdk(100, "deny-rdp", "Inbound", "Deny", "Tcp", "3389"),
    RuleRow.from_sdk(200, "allow-ssh-jump", "Inbound", "Allow", "Tcp", "22"),
]

for row in sorted(rows):
    print(row.priority, row.name, row.access, row.dest_port)

Prints:

100 deny-rdp Deny 3389
200 allow-ssh-jump Allow 22
400 allow-https Allow 443

Three rules fall out.

Rule one. order=True generates rich comparisons from fields. You do not hand-write __lt__ unless the generated tuple order is the wrong model.

Rule two. Field declaration order is sort key order. Priority first, or a dedicated sort_index first. Ports and names are passengers.

Rule three. compare=False is how a display field stays off the scale. Combined with frozen=True (optional here, useful for hashable census rows), the row becomes a stable value object. 09-04 already used frozen as "do not copy, make a value"; today uses it only as a companion to ordered rows.

default_factory (Ramalho Example 5-14) still applies when a mutable collection is a field. A rules: list[RuleRow] = field(default_factory=list) on an NsgRow is correct; rules: list = [] raises ValueError at class definition time.

§IV — What not to do

Do not sort with sorted(rows, key=lambda r: r.dest_port) and call it priority order. Do not put dest_port: int before priority and rely on luck. Do not set order=True and eq=False; Ramalho notes that combination raises. Do not mutate a frozen row to "fix" priority after insert; build a new row.

§V — Close instruction

Type the three-row example. Confirm sorted(rows) matches ascending priority. Then swap dest_port ahead of priority without compare=False and watch the order break. Pair: Ops prints the association census; Cert names priority 100–4096 and first-match-wins at AZ-900 grain.