Back to Blog
Python

Python Object Equality: == vs is Explained

python object equality: Learn how Python compares objects with == and is, how to implement __eq__ and __hash__, and avoid common equality pitfalls.

pythonobject equality__eq____hash__identity comparisonpython operators
Illustration of two Python objects being compared with == and is, showing value equality versus identity.

When developers ask about python object equality, the first thing to understand is that Python has two distinct ways to compare objects: the == operator and the is operator. These serve different purposes, and mixing them up leads to subtle bugs that are hard to trace. This article explains the mechanics behind both, how to implement custom equality for your classes, and why __hash__ must stay consistent with __eq__.

How Python Compares Objects: == vs is

The == operator checks whether two objects are equal in value. The is operator checks whether two references point to the exact same object in memory. For example:

a = [1, 2, 3] b = [1, 2, 3] c = a print(a == b) # True, because the lists have the same elements print(a is b) # False, because they are different list objects print(a is c) # True, because c references the same list as a

is compares identity, which is the object's memory address in CPython. == compares the result of the __eq__ method, which by default falls back to identity if not overridden. For immutable built-in types like integers and strings, == and is often appear to behave the same because Python interns small integers and short strings, but relying on that is fragile. The correct rule: use is when you need to check if two names refer to the same object (e.g., comparing to None), and use == when you need value equality.

Default Equality: Identity-Based Comparison

If you define a class without overriding __eq__, Python uses the default implementation inherited from object. That implementation compares identity, meaning two instances are equal only if they are the same object. Consider:

class Point: def __init__(self, x, y): self.x = x self.y = y p1 = Point(1, 2) p2 = Point(1, 2) print(p1 == p2) # False, because they are distinct objects

This default behavior is often not what you want for data-centric classes. Two points with the same coordinates should logically be equal. To change that, you override __eq__.

Implementing eq for Value Equality

To make instances of your class compare by value, define __eq__ inside the class. The method takes self and other, and returns True when the objects are considered equal. A typical implementation checks that the other object is of the same type, then compares the relevant attributes:

class Point: def __init__(self, x, y): self.x = x self.y = y def __eq__(self, other): if not isinstance(other, Point): return NotImplemented return self.x == other.x and self.y == other.y

Returning NotImplemented instead of False when the type does not match allows Python to try the reflected operation on the other object. This is important for mixed-type comparisons. With this implementation, Point(1, 2) == Point(1, 2) returns True. You may also want to implement __lt__ and other ordering methods if you need sorting, but that is separate from equality.

The hash Contract: Why It Matters

When you override __eq__ in a class, Python automatically sets __hash__ to None unless you explicitly define it. This is because hashable objects must have a hash value that remains constant for the object's lifetime, and two equal objects must have the same hash. If you make objects equal by value but keep the default identity-based hash, they would violate the contract when used in dictionaries or sets. For example:

class Point: def __init__(self, x, y): self.x = x self.y = y def __eq__(self, other): if not isinstance(other, Point): return NotImplemented return self.x == other.x and self.y == other.y p = Point(1, 2) print(hash(p)) # TypeError: unhashable type: 'Point'

To make the class hashable, implement __hash__ based on the same attributes used in __eq__. The standard approach is to return a hash of a tuple of those attributes:

class Point: def __init__(self, x, y): self.x = x self.y = y def __eq__(self, other): if not isinstance(other, Point): return NotImplemented return self.x == other.x and self.y == other.y def __hash__(self): return hash((self.x, self.y))

Now Point(1, 2) can be used as a dictionary key, and lookups work correctly because equal points produce the same hash. If you ever need to make a class mutable but still use it in sets, you must avoid changing the attributes that contribute to the hash after insertion; otherwise, the object will be lost in its container.

Common Equality Pitfalls in Python

Several mistakes appear frequently when working with python object equality. One is comparing floats with == directly, which can fail due to floating-point precision. Instead, use math.isclose for approximate comparisons. Another pitfall is forgetting to handle NotImplemented correctly in __eq__, leading to asymmetric equality. For instance, if a == b works but b == a returns False because the other type is not handled, you create an inconsistency. Always return NotImplemented for unsupported types so Python can try the reverse comparison.

A third issue is relying on is for value comparison. Since is checks identity, it works for interned small integers but fails for larger numbers or strings created at runtime. For example:

a = 1000 b = 1000 print(a is b) # False in most implementations, because 1000 is not interned

Use == for numeric and string comparisons. Finally, when you override __eq__, remember that you also affect the default __hash__. If you do not redefine __hash__, your class becomes unhashable, which breaks usage in sets and as dictionary keys.

Performance Impact of Equality and Hashing

Equality and hashing directly affect the performance of dictionaries and sets. These collections use the hash value to locate a bucket, then call __eq__ to resolve collisions. If __hash__ is expensive or __eq__ does heavy work, lookups become slower. For example, a class whose __eq__ compares large lists or performs database queries will make dictionary operations noticeably slower. Keep both methods lightweight and consistent. Also, if you override __eq__ but leave __hash__ as None, every insertion into a set raises a TypeError, which is a runtime failure you want to avoid.

Another performance consideration is that immutable objects with a cached hash can speed up repeated lookups. If your class is truly immutable, you can compute the hash once in __init__ and store it, then return that stored value from __hash__. This avoids recomputing the hash every time the object is used in a hash-based collection. However, this only works if the object never changes after creation.

Guidelines for Robust Equality Implementation

When implementing equality for your own classes, follow these practical guidelines. First, always override __eq__ and __hash__ together when you need value equality. Second, use isinstance(other, type(self)) or a more specific type check to avoid subclasses causing unexpected behavior. Third, return NotImplemented for incompatible types so Python can handle the fallback. Fourth, make __hash__ depend only on the same attributes that __eq__ uses, and ensure those attributes are immutable. Fifth, for mutable objects, either avoid using them as dictionary keys or document that changing them after insertion breaks the container. Finally, use the functools.total_ordering decorator if you also need ordering methods, but note that it only supplies the comparison operators, not __hash__.

A concrete example that combines these ideas:

from functools import total_ordering @total_ordering class Money: def __init__(self, amount): self.amount = amount def __eq__(self, other): if not isinstance(other, Money): return NotImplemented return self.amount == other.amount def __lt__(self, other): if not isinstance(other, Money): return NotImplemented return self.amount < other.amount def __hash__(self): return hash(self.amount)

This class is immutable in practice (though not enforced), so its hash is safe. It compares by amount, and total_ordering fills in <=, >, and >= from __eq__ and __lt__. The hash is consistent with equality because equal amounts produce equal hashes.

Understanding python object equality means knowing when to use == versus is, how to override __eq__, and why __hash__ must align with it. Applying these rules consistently prevents subtle bugs and keeps your code predictable when objects are used in collections or compared across modules.

python object equality: Practical Usage and Code Examples | RYUSLOG DEV