Back to Blog
Python

Python ne: Implementing Not Equal for Custom Classes

python **ne**: Learn how the __ne__ method controls != in Python, why default behavior can be surprising, and how to implement it correctly for custom classes.

__ne__Python dunder methodsobject comparisonoperator overloadingcustom classes
Illustration of two Python objects being compared with a not-equal operator, highlighting the __ne__ method.

python ne 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 != operator does not automatically behave the way you might expect. The behavior depends on the __ne__ method, and if you only implement __eq__, the default handling of != can lead to subtle bugs. In Python, the ne method, officially __ne__, controls not-equal comparisons. Understanding how it interacts with __eq__ and the NotImplemented sentinel is essential for writing predictable, maintainable classes.

What ne Means in Python

The name ne is short for "not equal." In Python, the != operator invokes the __ne__ method on the left operand, passing the right operand as an argument. If __ne__ is not defined, Python falls back to the negation of __eq__ when possible. This fallback exists for convenience, but it can produce incorrect results when __eq__ returns NotImplemented or when equality is defined asymmetrically.

Consider a simple class representing a 2D point:

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

Here, __eq__ is defined, but __ne__ is not. What happens when you evaluate Point(1, 2) != Point(3, 4)? Python will attempt to call Point(1, 2).__ne__(Point(3, 4)). Since it doesn't exist, it looks for __eq__ on the same object and negates the result. In this case, __eq__ returns False, so != returns True. That seems correct. But the fallback is not always reliable.

Default Behavior of != Without __ne__

Python's default behavior for != when __ne__ is missing is to negate the result of __eq__, but only if __eq__ returns a boolean or NotImplemented. If __eq__ returns NotImplemented, Python tries the reflected operation on the right operand. If that also returns NotImplemented, the comparison falls back to identity comparison (is). This can lead to surprising results, especially when comparing objects of different types.

For example, if you compare a Point to an integer:

Point(1, 2) != 5

__eq__ is called with 5 as the argument. It returns NotImplemented because 5 is not a Point. Python then tries (5).__eq__(Point(1, 2)), which also returns NotImplemented. The final fallback is identity comparison, which returns True because the objects are different. So != returns True. That might be acceptable, but consider a case where __eq__ is defined to return False for a type mismatch instead of NotImplemented. Then != would return True as well, but the semantics differ.

The real problem arises when __eq__ is implemented incorrectly, such as returning False for all non-matching types without using NotImplemented. This can break the symmetric contract of equality and cause != to behave inconsistently.

Implementing __ne__ Correctly

To avoid relying on the fallback and to make your intent explicit, you should define __ne__ alongside __eq__. The correct implementation is straightforward: delegate to __eq__ and negate its result, but only when __eq__ returns a boolean. If __eq__ returns NotImplemented, __ne__ should also return NotImplemented so that Python can try the reflected operation.

Here is the recommended pattern:

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

This ensures that != behaves exactly as the logical negation of == for supported types, and it properly signals NotImplemented for unsupported types. Note that in Python 3, if you define __eq__ but not __ne__, the default __ne__ is actually synthesized from __eq__ by the interpreter, but only if __eq__ is defined. However, that synthesized version does not handle NotImplemented correctly in all cases. It simply negates the return value of __eq__ without checking for NotImplemented. So defining __ne__ explicitly is safer.

Handling NotImplemented and Asymmetric Comparisons

When you implement __ne__, you must respect the NotImplemented sentinel. If __eq__ returns NotImplemented, it means the comparison is not supported for the given operand type. Your __ne__ should propagate that signal so that Python can attempt the reflected operation on the other operand. Failing to do so can lead to incorrect results or unexpected exceptions.

Consider a class that defines equality with a custom type but only in one direction:

class Temperature: def __init__(self, celsius): self.celsius = celsius def __eq__(self, other): if isinstance(other, Temperature): return self.celsius == other.celsius if isinstance(other, (int, float)): return self.celsius == other return NotImplemented def __ne__(self, other): result = self.__eq__(other) if result is NotImplemented: return NotImplemented return not result

Here, Temperature(20) == 20 returns True, and Temperature(20) != 20 returns False. But what about 20 == Temperature(20)? Python first calls (20).__eq__(Temperature(20)), which returns NotImplemented. Then it tries Temperature(20).__eq__(20), which returns True. So 20 == Temperature(20) works. Similarly, 20 != Temperature(20) will call (20).__ne__(Temperature(20)), which is not defined, so it falls back to the negation of __eq__ on the int, which returns NotImplemented. Then Python tries Temperature(20).__ne__(20), which returns False (since __eq__ returned True). So the result is False, meaning they are equal. That is correct. But if __ne__ had not been defined, the fallback might have produced the same result, but with subtle differences when __eq__ returns NotImplemented for both sides.

Common Pitfalls When Defining __ne__

One common mistake is to implement __ne__ as return not self == other without considering NotImplemented. If __eq__ returns NotImplemented, not NotImplemented evaluates to False (because NotImplemented is truthy). This would incorrectly indicate that the objects are equal when the comparison is actually unsupported. For example:

class BadPoint: def __init__(self, x): self.x = x def __eq__(self, other): if isinstance(other, BadPoint): return self.x == other.x return NotImplemented def __ne__(self, other): return not self == other # Wrong: not NotImplemented -> False

Comparing BadPoint(1) != 5 would call __ne__, which evaluates self == 5. That calls __eq__, which returns NotImplemented. not NotImplemented is False, so != returns False, incorrectly claiming the objects are equal. This is a serious bug.

Another pitfall is forgetting to define __ne__ when you define __eq__ in a class that is used in sets or as dictionary keys. While the fallback works for simple cases, it can cause inconsistent behavior when __eq__ is not symmetric or when it returns NotImplemented. Always define __ne__ explicitly to maintain the contract.

Maintainability and Consistency in Custom Classes

Defining __ne__ explicitly improves maintainability by making the comparison behavior self-documenting. When another developer reads your class, they see both __eq__ and __ne__ together, and they can trust that != is the exact negation of ==. This is especially important in domain models where equality is based on business keys, such as an Order class comparing by order ID.

A consistent implementation also prevents subtle bugs when you later modify __eq__. If you forget to update __ne__, the two methods can drift apart. By delegating __ne__ to __eq__ and negating the result, you ensure they always stay in sync. This is a simple form of the DRY principle applied to comparison operators.

When to Avoid Defining __ne__ Manually

In many cases, you do not need to define __ne__ at all. If you are using a simple value object where __eq__ is based on a tuple of attributes, you can use @dataclass or NamedTuple, which generate both __eq__ and __ne__ automatically. For example:

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

The generated __eq__ and __ne__ correctly handle NotImplemented and type checks. Similarly, if you are using functools.total_ordering, you only need to define __eq__ and one other ordering method, and the decorator fills in the rest, including __ne__.

However, for classes with custom equality logic that cannot be expressed as a simple attribute comparison, manually implementing __ne__ is the right choice. The key is to follow the pattern of returning NotImplemented when the other operand is not supported, and to delegate to __eq__ to avoid duplication.

A final consideration is performance. The overhead of an extra method call in __ne__ is negligible compared to the cost of the actual comparison logic. If you are comparing millions of objects in a tight loop, the difference between an explicit __ne__ and the default fallback is usually not measurable. Focus on correctness first, and profile only if you have evidence that comparison is a bottleneck.

By understanding how __ne__ works and implementing it deliberately, you ensure that your custom classes behave predictably with the != operator, avoiding the subtle bugs that can arise from Python's default fallback behavior.

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