Back to Blog
Python

Python __eq__: Defining Object Equality

python **eq**: Learn how to implement Python's __eq__ method for custom classes, understand its interaction with __hash__, and avoid common pitfalls in equality compar...

pythondunder-methodsequalityhashobject-comparison
A visual metaphor for Python object equality showing two distinct objects with matching attributes connected by an equal sign, representing the __eq__ method.

python eq requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.

When you define a custom class in Python, the default equality behavior compares object identity, not value. Two instances are equal only if they are the same object. To make distinct instances compare equal based on their contents, you need to implement the __eq__ method. This article explains how to do that correctly, what happens to __hash__ when you do, and how to avoid the most common mistakes developers make with object equality.

What eq Does and Why It Matters

The __eq__ method is called whenever you use the == operator on two objects. Its default implementation compares id(self) == id(other), which is effectively identity comparison. For many classes, that is not what you want. A Person class with a name attribute should consider two instances equal if they have the same name, not just if they are the same object in memory.

Implementing __eq__ gives you control over what equality means for your class. This is critical when you use objects in sets, as dictionary keys, or when you need to compare data objects for testing or business logic. Without a proper __eq__, you cannot rely on == to reflect the logical equivalence of your objects.

A Minimal eq Implementation

A straightforward __eq__ implementation compares the attributes that define the object's identity. For a simple Point class, that means comparing x and y coordinates:

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

The isinstance check is important. Returning NotImplemented tells Python to try the reflected operation on the other operand, which is the correct behavior when the other object is not of the same type. If you simply return False for a different type, you may break symmetry in equality checks. For example, Point(1, 2) == (1, 2) should return False, but (1, 2) == Point(1, 2) would then also return False if the tuple's __eq__ is invoked and returns NotImplemented. Returning NotImplemented from your __eq__ allows Python to give the other object a chance to respond, which is the correct protocol.

The Relationship Between eq and hash

Python requires that objects that compare equal have the same hash value. This is a fundamental contract for dictionaries and sets. If you define __eq__ without also defining __hash__, Python sets __hash__ to None, making your class unhashable. That means you cannot use instances as dictionary keys or put them in a set.

To maintain hashability, you must implement __hash__ consistently with __eq__. The simplest approach is to hash the same tuple of attributes you compare in __eq__:

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))

If you do not need to use the objects in sets or as dictionary keys, you can leave __hash__ unset and accept that the class is unhashable. But if you plan to use them in collections, you must provide a consistent __hash__. A common mistake is to define __eq__ without updating __hash__, which silently makes the class unhashable. This often surfaces only when you try to add an instance to a set and get a TypeError: unhashable type: 'Point'.

Comparing Objects of Different Types

When you compare two objects of different types, you need to decide whether they can ever be equal. In most cases, they should not be. Returning NotImplemented for a type mismatch is the correct approach because it allows Python to try the other object's __eq__. If the other object also returns NotImplemented, Python falls back to identity comparison and returns False.

Consider a class that represents a numeric value and might want to compare equal to an int:

class Number: def __init__(self, value): self.value = value def __eq__(self, other): if isinstance(other, Number): return self.value == other.value if isinstance(other, int): return self.value == other return NotImplemented

This allows Number(5) == 5 to return True. However, this can lead to asymmetry if int does not know about Number. Python handles this by trying the reflected operation: when 5 == Number(5) is evaluated, int.__eq__ is called first. If it returns NotImplemented, Python calls Number.__eq__ with the arguments reversed. This works only if your __eq__ returns NotImplemented for types it cannot handle, not False. Returning False would prevent the reflected operation from being attempted.

Using Dataclasses to Generate eq

Writing __eq__ and __hash__ manually is repetitive, especially for classes that are mostly data containers. Python's dataclasses module can generate both methods for you. By default, a dataclass generates __eq__ that compares all fields, and it generates __hash__ if eq=True and frozen=True (or if you explicitly set unsafe_hash=True).

from dataclasses import dataclass @dataclass class Point: x: int y: int

This produces a Point with __eq__ that compares x and y, and __hash__ that hashes the tuple (x, y). The generated __eq__ also returns NotImplemented when the other object is not the same type, which is the correct behavior.

If you need to exclude certain fields from equality, you can use the field(compare=False) option. This is useful when you have a field like an internal ID that should not affect equality, or a cached value that is derived from other fields. Dataclasses give you a concise way to define equality without writing boilerplate, and they are the recommended approach for most data-centric classes.

Common Pitfalls and How to Avoid Them

One frequent mistake is forgetting to update __hash__ when you define __eq__. As mentioned, this makes the class unhashable. If you need hashability, either implement __hash__ manually or use a dataclass with frozen=True.

Another pitfall is comparing mutable attributes. If an object's __eq__ depends on a mutable field, the object's hash changes when that field changes. If the object is already in a set or used as a dictionary key, this breaks the hash-based lookup and can lead to data loss or incorrect behavior. To avoid this, use immutable attributes for equality and hashing. If you need mutable objects, do not use them as dictionary keys or set members.

A third issue is defining __eq__ that is not symmetric. For example, if you return True when comparing a subclass instance to a base class instance but not the other way around, you can violate the equality contract. Always ensure that a == b implies b == a for the types you control. Returning NotImplemented for unknown types helps maintain symmetry because it delegates the decision to the other object.

Performance Considerations for Equality Checks

Equality checks can be expensive when objects have many attributes or when they are compared frequently in loops. The cost of __eq__ is proportional to the number of attributes you compare. For large objects, you can optimize by comparing cheaper attributes first. For example, if you have a class with a numeric ID and a large string field, compare the ID first because integer comparison is faster than string comparison.

Another consideration is hash caching. If your object is immutable and you compute a hash that involves expensive operations, you can cache the hash value in a private attribute to avoid recomputing it. This is particularly useful when the object is used as a dictionary key and __hash__ is called many times. However, caching a hash requires that the object be truly immutable, otherwise the cached value becomes stale.

When using dataclasses, the generated __eq__ compares all fields in order. If you have a field that is expensive to compare, you can mark it with compare=False to exclude it from equality checks. This reduces the cost of == operations but means that two objects with different values in that field can still compare equal, which may or may not be desirable depending on your use case.

Choosing Between Manual Implementation and Dataclasses

For simple data containers, dataclasses are almost always the better choice. They reduce boilerplate, generate correct __eq__ and __hash__, and make the code easier to read. Manual implementation is necessary only when you need custom equality logic that dataclasses cannot express, such as comparing only a subset of fields based on a condition, or when you need to inherit from a class that already defines __eq__ and you want to extend it.

If you are working with legacy code or a class that has complex initialization logic, adding __eq__ manually is straightforward. The key is to always pair it with a consistent __hash__ and to return NotImplemented for unsupported types. By following these rules, you ensure that your objects behave correctly in sets, dictionaries, and any other context that relies on equality and hashing.

python **eq**: Practical Usage and Code Examples | RYUSLOG DEV