Skip to content

Disambiguate unions independent of member class order (#230)#765

Open
vineethsaivs wants to merge 1 commit into
python-attrs:mainfrom
vineethsaivs:fix/disambiguate-order-independent
Open

Disambiguate unions independent of member class order (#230)#765
vineethsaivs wants to merge 1 commit into
python-attrs:mainfrom
vineethsaivs:fix/disambiguate-order-independent

Conversation

@vineethsaivs

Copy link
Copy Markdown

Fixes #230.

Problem

create_default_dis_func (aka create_uniq_field_dis_func) fails to disambiguate a valid union depending on the order its member classes are passed in.

from attrs import define
from cattrs import Converter
from cattrs.disambiguators import create_default_dis_func

@define
class A: ab: int; a: str
@define
class B: ab: int; bc: int
@define
class C: bc: int

c = Converter()
create_default_dis_func(c, A, B, C)   # ok
create_default_dis_func(c, B, A, C)   # TypeError: <class 'C'> has no usable non-default attributes

A valid assignment exists (A -> a, B -> ab, C -> bc), and 3 of the 6 orderings find it while the other 3 raise.

Root cause

The "unique keys" branch resolves discriminator fields in a single fixed-order pass. For each class it subtracts the fields of every not-yet-pinned class and takes what's left as that class's unique fields. A class whose fields are all shared with another still-unpinned class therefore gets no unique field and is pushed into the single fallback slot; a second such class then raises. Since the processing order is fixed up front (sorted once by field count) and never re-selects which class to pin next, whether the valid assignment is found depends on the input order. In the example, if C is processed while B is still unpinned, C's only field bc is shared with B, so C has no unique field even though pinning B on ab first would free bc for C.

Fix

Resolve unique fields iteratively to a fixpoint. On each pass, pin every class that has a non-default field unique among the classes not yet pinned. Pinning a class removes its fields from consideration, which can make a previously ambiguous class uniquely identifiable, so passes repeat until one pins nothing. The single class that still cannot be pinned becomes the fallback; more than one remaining is a genuine ambiguity and still raises TypeError. This is order-independent and contained to the one function; the discriminator-literal path and both dis_func variants are unchanged.

The generated dis_func still checks keys in insertion (pin) order, and that stays correct: a class is pinned only on a field unique among the classes still unpinned on that pass, so its key cannot appear in the payload of any class pinned later or in the fallback, and an earlier-pinned class's key cannot appear in a later class's payload.

Test

test_disambiguation_is_order_independent builds the union above and asserts that all 6 orderings both succeed and route each member's payload back to the correct class. It fails on the current code (3 of 6 orderings raise) and passes with this change. The existing test_edge_errors cases (no fields, shared single field, unhelpful discriminator, and the fallback cases) still pass, as do the union suites.

create_default_dis_func resolved unique discriminator fields in a single
fixed-order pass: for each class it subtracted the fields of every
not-yet-pinned class, so a class whose fields were all shared with another
unpinned class got no unique field and was forced into the single fallback
slot. A second such class then raised TypeError, even when a valid
assignment existed. Because the order was fixed up front, whether it was
found depended on the order the classes were passed in.

Resolve unique fields iteratively to a fixpoint instead: on each pass, pin
every class that has a non-default field unique among the classes not yet
pinned; pinning a class can make a previously ambiguous one uniquely
identifiable, so repeat until a pass pins nothing. The single class that
still cannot be pinned becomes the fallback, and more than one is a genuine
ambiguity (still a TypeError). Fixes python-attrs#230.
@vineethsaivs vineethsaivs force-pushed the fix/disambiguate-order-independent branch from c2948bf to b2d0716 Compare July 6, 2026 06:32
@codspeed-hq

codspeed-hq Bot commented Jul 6, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 64 untouched benchmarks


Comparing vineethsaivs:fix/disambiguate-order-independent (b2d0716) with main (3d07082)1

Open in CodSpeed

Footnotes

  1. No successful run was found on main (d7de3c2) during the generation of this report, so 3d07082 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

@Tinche Tinche self-requested a review July 6, 2026 20:29
@Tinche

Tinche commented Jul 6, 2026

Copy link
Copy Markdown
Member

Interesting! LGTM. Will re-review in a couple of days and maybe merge. Thanks.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

disambiguators.create_uniq_field_dis_func fails to disambiguate ambiguous unions depending on parameter order

2 participants