skip to content

With no `__eq__` defined, how does `==` behave on class instances?

level: juniorimportance: should knowfreq 58%

answer

  1. Nothing is missing; something is inherited
  2. object supplies three of them
  3. Same object, or not equal
  4. NotImplemented, then an identity fallback
  5. != is derived since Python 3

basics

~10 s

A class with no eq inherits object.eq, which compares identity: x == y is True only when both names refer to the very same object. Two instances built from identical data are still unequal.

solid answer

~40 s

Every class inherits `__eq__`, `__ne__` and `__hash__` from `object`, so equality is never missing — it just defaults to identity. `object.__eq__` returns `True` when the operands are the same object and `NotImplemented` otherwise; when the reflected call on the right operand also declines, the `==` operator falls back to an identity comparison and yields `False`. So two instances holding identical attribute values compare unequal, and they hash differently too, because `object.__hash__` is derived from the object's `id()`. `!=` needs no work from you: since Python 3, `object.__ne__` calls `__eq__` and inverts any result other than `NotImplemented`. That default is the right one for entity-like objects — an open connection, a running job — and the wrong one for value objects, which is why you override it there.

code

pycon · 11 lines
pycon
>>> class Point:
...     def __init__(self, x, y):
...         self.x, self.y = x, y
...
>>> a, b = Point(1, 2), Point(1, 2)
>>> a == b, a == a, a != b
(False, True, True)
>>> len({a, b})
2
>>> Point.__eq__ is object.__eq__
True

go deeper

for a junior

Be ready to state that the inherited equality compares identity, and to predict that two separately constructed instances with the same attributes are unequal and count as two distinct set members.

for a middle

Explain the mechanism: object.__eq__ returns NotImplemented for anything but the same object, the reflected call is tried, and the operator falls back to identity. Note that __ne__ is derived from __eq__ in Python 3.

for a senior

Show judgement about which classes should keep identity equality — connections, jobs, sessions — and which are value objects that must override it. Point out that inherited identity hashing is what makes plain classes usable as keys.

for a principal

Own the convention across a codebase: decide where value semantics are allowed, since every value type carries an equality, hashing and mutation contract that reviewers must police, and identity semantics carry none of that cost.

## The default is not "missing", it is identity Every class in Python inherits from `object`, and `object` already supplies `__eq__`, `__ne__` and `__hash__`. A class that defines none of them therefore does not lack equality; it has a specific, well-defined equality, and that equality is **identity**: two names are equal only when they refer to the same object in memory. ## What `object.__eq__` actually does `object.__eq__(self, other)` returns `True` when `self is other`, and returns the singleton `NotImplemented` in every other case. `NotImplemented` is not `False` and it is not an exception — it is the object model's way of saying "I decline to answer." When the left operand declines, the interpreter tries the reflected call, `other.__eq__(self)`. If that also returns `NotImplemented` — which it does when the right operand is another plain object — the `==` operator itself finishes the job by comparing identity, and the expression evaluates to `False`. Two things follow. First, `==` between plain instances never raises: unlike ordering operators such as `<`, which raise `TypeError` when neither side knows how to compare, equality always has an identity fallback. Second, comparing your instance to an integer, a string or an unrelated class is simply `False` rather than an error. ## `!=` comes free In Python 3, `object.__ne__` delegates to `__eq__` and inverts the result, passing `NotImplemented` through untouched. You therefore almost never write `__ne__` by hand — defining `__eq__` alone gives you a consistent `!=`. This is a genuine language change worth knowing: under Python 2, `__ne__` was independent, and classes that defined only `__eq__` had an `!=` that silently fell back to identity, producing the notorious "`a == b` and `a != b` are both true" bug. The delegating behaviour has been stable across the whole Python 3 line and is unchanged in 3.14. ## `__hash__` comes free too `object.__hash__` derives a value from the object's `id()`. Distinct objects therefore almost always hash differently, and identity hashing agrees perfectly with identity equality: since the only pair that compares equal is an object with itself, the rule "equal objects must hash equal" cannot be violated. That is why a plain class works as a `dict` key or a `set` member with no effort from you — and why overriding `__eq__` alone breaks that, which is a separate question. ## The consequence to say out loud Two instances constructed from the same data are two different keys. Put them both in a `set` and the set has two members. Look one up in a `dict` using a freshly constructed "same" object and you get a `KeyError`. Compare two objects read out of two different rows of the same record and you get `False`. Candidates who assume that `==` compares attributes by default are surprised by all three. You can check what a class inherited: `MyClass.__eq__ is object.__eq__` is `True` when nothing overrode it, and `MyClass.__hash__ is object.__hash__` likewise. ## When the default is what you want Identity equality is correct for **entity** semantics — objects whose meaning is "this particular thing": an open network connection, a thread, a running job, a cache instance, a widget on screen. Two of them are two distinct things even when configured identically, so identity is not a compromise, it is the truth, and it comes for free with a working hash. Override equality for **value** semantics — objects whose meaning is entirely their contents: a point, a money amount, a version number, a parsed record. There, two objects with the same fields *are* the same value, and identity equality is a bug waiting for the first time someone compares or de-duplicates them. ## Inheritance and `is` Equality is looked up through the MRO like any other method, so a subclass gets whatever the nearest ancestor defines; a subclass of a class with a value-based `__eq__` inherits value equality, including the traps that come with comparing across the hierarchy. Finally, note that `is` and `==` coincide for a class with the default `__eq__`, but they are not the same operator. `is` is a fixed identity check that no class can override or intercept; `==` is a protocol call that any class can redefine. Relying on `is` because "it works today" breaks the moment someone adds an `__eq__`.

  • If `object.__eq__` returns `NotImplemented` rather than `False`, why does `x == y` still evaluate to `False`?
    `NotImplemented` means "I decline", so the interpreter offers the right operand a turn by calling its reflected `__eq__`. When both sides decline, the `==` operator supplies its own last-resort answer: it compares identity. The `NotImplemented` singleton itself never reaches your code from a comparison expression — you only see it if you call the dunder directly.
  • Do you ever need to write `__ne__` yourself in modern Python?
    Almost never. `object.__ne__` calls `__eq__` and inverts anything that is not `NotImplemented`, so a single `__eq__` gives a consistent `!=`. The rare exceptions are types with deliberately non-boolean comparison semantics, where `!=` must produce something other than the logical negation of `==`; writing both by hand otherwise just creates an opportunity for them to disagree.
  • Why is `is` not a safe substitute for `==` even when a class has the default equality?
    They agree only by coincidence there. `is` is a fixed identity test that cannot be overridden or intercepted, while `==` is a protocol call. The day someone adds a value-based `__eq__` — or the class is swapped for one that has it — every `is` comparison silently keeps the old meaning. Reserve `is` for `None`, `True`, `False` and sentinel objects.

By default an object is identified like a person by their passport number, not by their name and date of birth: two people with identical details are still two people until you decide, explicitly, that the details are what matter.

saying these in an interview costs you the question

  • Claims `==` compares attributes by default
  • Says a class without `__eq__` cannot be compared at all
  • Thinks `==` on unrelated types raises TypeError
  • Believes `__ne__` must be written alongside `__eq__`
  • Treats `is` and `==` as interchangeable for objects
  • Assumes two identical instances are one `set` member

context