Skip to content

Match filter attributes by identity instead of equality#1584

Open
PranavMishra28 wants to merge 1 commit into
python-attrs:mainfrom
PranavMishra28:filters-match-attribute-by-identity
Open

Match filter attributes by identity instead of equality#1584
PranavMishra28 wants to merge 1 commit into
python-attrs:mainfrom
PranavMishra28:filters-match-attribute-by-identity

Conversation

@PranavMishra28

Copy link
Copy Markdown

What was broken

attrs.filters.include / exclude accept types, field-name strings, and Attribute instances. The Attribute instances were stored in a frozenset and matched with attribute in attrs, i.e. by equality. But Attribute.__eq__/__hash__ ignore the owning class, so two identically-configured same-named attributes on different classes compare equal:

@attr.s
class Robber:
    repeated = attr.ib(default=1)
    only_robber = attr.ib(default=2)

@attr.s
class Cop:
    repeated = attr.ib(default=1)   # identically configured
    only_cop = attr.ib(default=4)

attr.asdict(Cop(), filter=attr.filters.exclude(attr.fields(Robber).repeated))
# -> {'only_cop': 4}   # Cop.repeated wrongly dropped

Excluding (or including) one class's attribute silently affects an identically-configured attribute of an unrelated class.

Why it matters

This is the behavior tracked in #864, which is labeled Bug. As noted there, Attribute instances are independent of the classes they're defined on, so equality-based matching "is absolutely not what people would expect." attr.fields(A).x and attr.fields(B).x are distinct objects, so identity is the correct test.

What the fix does

_split_what keeps the passed attributes in a tuple (instead of a frozenset), and include/exclude match them with any(attribute is a for a in attrs) — by identity. Since fields() returns the same cached Attribute object for a given class, the intended attribute still matches, while an equal-but-distinct attribute on another class no longer does.

What it deliberately does not change

  • Type and field-name-string matching are untouched. The cross-class "drop this field name from many classes" use case raised in asdict filter exclude list matches on name instead of attribute #864 is already served by passing the name string (exclude("repeated")), which matches by name across classes.
  • Public include/exclude signatures and the .pyi stub are unchanged.

Note on compatibility

This narrows Attribute matching from equality to identity. Anyone who (perhaps unknowingly) relied on the equal-but-distinct cross-class match would see a behavior change; per the issue this was considered a bug rather than intended behavior, so I filed it under change. Happy to reclassify as breaking if you'd prefer.

Testing

  • New identity regression tests for both include and exclude (equal-but-distinct attribute on another class is not matched); updated the _split_what test for the tuple.
  • Full suite green: 1383 passed, 9 skipped, 1 xfailed; ruff check and ruff format --check clean. Added changelog.d/864.change.md.

Fixes #864

Developed with Claude Code; reviewed and tested by Pranav before marking ready for review.

attrs.filters.include/exclude stored the passed Attribute instances in a
frozenset and tested membership with `attribute in attrs`. Attribute
equality ignores the owning class, so two identically-configured
attributes on different classes compare equal - filtering by one class's
attribute also matched the other class's. Keep the attributes in a tuple
and match them by identity instead. Field-name strings still match by
name across classes, which covers the cross-class use case.

Fixes python-attrs#864

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@PranavMishra28 PranavMishra28 marked this pull request as ready for review July 7, 2026 20:57
@PranavMishra28

Copy link
Copy Markdown
Author

just wanted to highlight that this is ready for a look whenever you have a moment. It directly fixes the equality bug in #864 and all 19 checks are passing green.

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.

asdict filter exclude list matches on name instead of attribute

1 participant