Back to Blog
Python

Python Object Identity vs Equality: `is` and `==`

python object identity vs equality: Understand the difference between object identity and equality in Python, when to use `is` versus `==`, and how custom equality aff...

Python identityPython equalityis vs ==Python objects__eq__ and __hash__
Illustration of two Python objects compared with identity and equality operators, showing a pointer to the same object versus separate objects with equal values.

When comparing Python objects, developers often confuse object identity with equality. The distinction between python object identity vs equality is fundamental: identity asks "is this the same object?" while equality asks "do these objects have the same value?" This article explains the mechanics behind is and ==, how Python defines both, and where each comparison is appropriate in real code.

The Core Difference Between Identity and Equality

In Python, every object has a unique identity, which is an integer that remains constant for the object's lifetime. You can retrieve it with id(). The is operator compares these identities directly: a is b is true only if a and b refer to the same object in memory.

Equality, on the other hand, is defined by the __eq__ method. The == operator invokes this method to decide whether two objects should be considered equal based on their contents or state. For many built-in types, equality compares values, but the exact behavior depends on the type's implementation.

a = [1, 2, 3] b = [1, 2, 3] print(a == b) # True, values match print(a is b) # False, different list objects

Here a and b are two distinct list objects with the same contents. == returns True because list.__eq__ compares element-wise. is returns False because they occupy different memory addresses.

How Python Determines Identity: The is Operator

The is operator performs a direct pointer comparison. It does not call any method or inspect object contents. For CPython, this is equivalent to comparing id(a) == id(b). Because it avoids method dispatch, is is extremely fast.

Identity is meaningful when you need to know whether two names refer to the exact same object. This is common when:

  • Checking for None (the canonical singleton).
  • Comparing sentinel values you defined yourself.
  • Verifying that a function argument is the same instance passed by a caller.
value = None if value is None: print("value is None")

Using is None is idiomatic because None is guaranteed to be a singleton. There is exactly one None object in a Python process, so identity comparison is correct and faster than == None.

How Python Determines Equality: The == Operator

The == operator delegates to the left operand's __eq__ method. If that method returns NotImplemented, Python tries the right operand's __eq__. If both fail, the default behavior falls back to identity comparison.

For built-in types, equality is usually value-based. Numbers compare numerically, strings compare lexicographically, and lists/tuples compare element-wise. However, the semantics can differ:

print(1 == 1.0) # True, numeric equality print(1 == True) # True, because bool is a subclass of int print("a" == "a") # True, string content

Equality is not transitive across types in all cases. For instance, 1 == 1.0 is true, and 1.0 == True is true, but 1 == True is also true. This works because numeric comparison is well-defined. But custom classes can break transitivity if __eq__ is not implemented carefully.

Default Behavior: When is and == Match

For many built-in immutable types, small integers and short strings are interned. CPython caches integers from -5 to 256, and may reuse string objects that look like identifiers. As a result, a is b can be True for these values even when a and b are separate variables.

x = 256 y = 256 print(x is y) # True, due to integer interning p = "hello" q = "hello" print(p is q) # True, CPython may intern short strings

This behavior is an implementation detail and should never be relied upon. The language specification only guarantees that is compares identity, not that values are interned. Code that depends on interning for strings or integers is fragile and may break on a different Python implementation or even a different version.

When you create a new object explicitly, identity and equality diverge:

r = [1, 2] s = [1, 2] print(r is s) # False print(r == s) # True

Mutable containers like lists and dictionaries always create new objects, so is is almost always False for separately constructed instances.

Customizing Equality with __eq__ and __hash__

When you define a class, the default __eq__ compares identity. If you want value-based equality, you must override __eq__. Doing so also affects hashing: any class that defines __eq__ but not __hash__ becomes unhashable, because Python sets __hash__ to None to maintain the invariant that equal objects have equal hashes.

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

With this implementation, two Point instances with the same coordinates are equal and share the same hash. This allows them to be used as dictionary keys or set members without violating the hash contract.

If you override __eq__ but not __hash__, instances become unhashable:

class BrokenPoint: def __init__(self, x): self.x = x def __eq__(self, other): return self.x == other.x p = BrokenPoint(1) # hash(p) # raises TypeError: unhashable type: 'BrokenPoint'

This is a common source of bugs when developers add __eq__ for comparisons and then try to use the objects in a set or dictionary. The fix is to also define __hash__, typically based on the same attributes used in __eq__.

Common Pitfalls: Strings, Integers, and None

One of the most frequent mistakes is using is to compare strings that are not interned. For example, when a string is built dynamically, is may return False even if the content is identical:

a = "hello" b = "he" + "llo" print(a is b) # False in CPython, because b is a new object print(a == b) # True

The same applies to integers outside the cached range:

x = 1000 y = 1000 print(x is y) # False in CPython print(x == y) # True

For None, always use is None rather than == None. This is both faster and clearer, and it avoids potential issues if a class overrides __eq__ to treat None as equal to itself. The same advice applies to comparing against custom sentinels.

Another pitfall is relying on == for object identity when you actually need to ensure that two references point to the same object. For example, in a cache or memoization system, you might want to know if a returned object is the same instance as the one stored. Using == could return True for distinct but equal objects, which would defeat the purpose.

Performance and Runtime Cost: Why is Is Faster

Because is performs a direct pointer comparison, it avoids method calls and attribute lookups. In tight loops or when comparing many objects, this difference can be measurable. For example, checking if item is None is faster than if item == None because the latter could invoke None.__eq__ (though in practice None is a singleton and the default __eq__ is fast, the principle holds).

More importantly, is does not trigger user-defined __eq__ methods. If a class has a complex equality implementation that performs deep comparisons, using is as a fast path can avoid unnecessary work. A common pattern is to check identity first:

def are_equal(a, b): if a is b: return True return a == b

This is safe because if two objects are identical, they are necessarily equal by definition. The identity check short-circuits and avoids the more expensive equality computation.

However, you should not replace all == with is for performance. is is only correct when you specifically need identity. Using it for value comparison will produce wrong results for distinct objects with equal values.

Choosing Between is and == in Practice

Use is when:

  • You are comparing against a known singleton like None or a custom sentinel.
  • You need to verify that two references point to the exact same object (e.g., checking if a function returned a cached instance).
  • You are implementing a fast path before a potentially expensive == comparison.

Use == when:

  • You want to compare values, regardless of whether they are the same object.
  • You are working with numbers, strings, lists, or other built-in types where value equality is the expected semantic.
  • You have defined __eq__ on a custom class to provide meaningful equality.

A practical decision rule: if the question is "are these two things the same object?" use is; if the question is "do these two things have the same value?" use ==. This distinction becomes critical when dealing with mutable objects, where two objects may have equal contents at one moment but diverge later. Identity remains stable, while equality can change.

For custom classes, ensure that __eq__ and __hash__ are consistent. If you override __eq__ without __hash__, you break the ability to use instances in sets and as dictionary keys. If you override both, make sure that equal objects always produce the same hash, and that unequal objects are unlikely to collide. The simplest approach is to hash a tuple of the same attributes used in __eq__.

Finally, remember that interning is an implementation detail. Never write code that assumes a is b for integers or strings based on their value. The language guarantees only that is compares identity, and that == compares equality according to the type's definition. Rely on those guarantees, not on CPython's caching behavior.

python object identity vs equality: Practical Usage and Code | RYUSLOG DEV