python-attrs / python-attrs/attrs

No way to make instances of different classes equal without rewriting `__eq__()` entirely

Open
#1,500 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Python
Stars
5.8k
Forks
480
Avg merge
2h 15m
Merged PRs (30d)
2

Description

Motivation and Problem

In my code, it's a common idiom to have a two classes called, e.g., SomethingMutable and SomethingFrozen; these are both attrs classes decorated with @attrs.define and @attrs.frozen, respectively. Their field definitions are "the same" with minor type differences to account for mutability (list vs. tuple, set vs. frozenset, etc.)

I'd like instances of SomethingMutable and SomethingFrozen to compare equal IFF every the values for the defined fields are all equal.

What doesn't work

The attrs-generated __eq__() always checks instance types are the same before checking that the field values match, meaning that instances of different attrs classes can never compare equal. Consequently, overriding __eq__() and calling super() isn't helpful.

There's no obvious way to leverage the attrs-generated, field-by-field comparisons currently in __eq__() unless both instances are exactly the same type.

Possible approaches

It would be very helpful if there were a way to tell attrs to ignore the classes when comparing, and consider only those fields defined by the left instance's class.

This could take a number of forms, such as:

  • __eq__() could take an optional keyword argument ignore_type: bool, intended for use via super()
  • attrs could break __eq__() into two steps: one to check instance types, and one to check field values
    • classmethod[__attrs_types_equal__(cls, other: type) -> bool]
      would replace today's __eq__()'s first step, i.e., roughly: if type(other) is not type(self): return False
    • __attrs_fields_equal__(self, other: Any) -> bool
      would check that every field in self is matched by an attribute of other which has an equal value (or whatever attrs.field(eq=...) says to do)
      • for example
        • if class Child(Parent): ... adds at least one field not found in Parent
        • and given: c: Child; p: Parent
        • then it would typically be true that p.__attrs_fields_equal__(c),
          • since p._afe_(c) considers only fields in p, all of which are also present in c (per the LSP/Liskov Substitution Principle),
        • ... but typically false that c.__attrs_fields_equal__(p)
          • since c._afe_(p) considers only fields in c, some of which are absent from p (by assumption)
    • __eq__() could be replaced by just a few, static lines:
      return (
              self.__attrs_types_equal__(type(other))  and
              self.__attrs_fields_equal__(other)
              )
      
      • breaking up __eq__() might carry a performance penalty
        • but I think it should usually be possible to detect cases where a bifurcated implementation is called for, add an override flag to attrs.define() (etc.) for edge cases, and use a unified implementation (as we do today) where we can
        • __eq__() could also check whether type(self).__attrs_types_equal__ is the unmodified attrs-provided version (identity comparison); and, if so, use an inlined copy of the same generated code to save the overhead of the method call
          • (...and similarly for __attrs_fields_equal__())
          • this should be safe even even where there's monkeypatching

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

The issue names no source file or test entry point. Start by reviewing attrs' generated eq behavior and the proposed API alternatives, then establish a concrete, backwards-compatible scope; done should include agreed cross-class equality semantics for mutable and frozen attrs classes and coverage for the relevant field comparisons.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
tooling
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Needs clarification
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.