Identity, equality and aliases

Know when names share objects and when value comparison is the contract

Python's `==` and `is` answer different questions. `==` dispatches through `__eq__` and can be arbitrarily expensive, comparing value not identity. `is` compares object pointers directly and cannot be overloaded. When `__eq__` returns `NotImplemented`, Python interprets that as unsupported and tries the reflected operation on the other operand. If both return `NotImplemented`, the comparison falls back to identity. CPython interns small integers (typically -5 to 256) and some short strings as an optimization, but that is an implementation detail not a language guarantee. Use `is None` for singleton checks because `None` has a guaranteed single instance. Sentinel objects with `object()` follow the same pattern. Understanding aliasing explains why mutating one name can affect another. <a href="/language-mutable-defaults">See how aliasing creates the mutable default argument problem</a>. <a href="/language-dunder-methods">Learn how __eq__ and __hash__ work together</a>.

Understand.
Visualize.
Master.

Python in Depth

An interactive engineering reference for Python internals

Quick note

Names are sticky notes, not boxes.

:)
TABLE OF CONTENTS
2.1Identity, equality and aliases

Know when names share objects and when value comparison is the contract

You wrote a = [1, 2], then b = a, then c = [1, 2]. Mutating a changed b but not c. Every Python developer hits this early and wonders whether == and is are the same thing. They are not — and the difference is central to the object model.

Think of Python variables like sticky notes, not boxes. Assignment does not put a value into a container — it sticks a name onto an object. Multiple sticky notes can point to the same object. == asks whether two objects consider themselves equal. is asks whether two sticky notes point to the exact same object — a single machine instruction, no method dispatch.

Core answer

Use == for value equality and is for object identity. Identity asks whether two references point to the same object. Equality asks whether two objects compare as equivalent under their type's comparison rules.

# [CURRENT - 3.10-3.14] Works on Python 3.x
a = [1, 2, 3]
b = [1, 2, 3]
alias = a
print(a == b) # True
print(a is b) # False
print(alias is a) # True
Mechanism and data model

is is a direct identity comparison and cannot be overloaded. == dispatches through rich comparison methods such as __eq__, which means its cost and semantics depend on the type.

# [CURRENT - 3.10-3.14] Works on Python 3.x
class UserId:
def __init__(self, value):
self.value = value
def __eq__(self, other):
if not isinstance(other, UserId):
return NotImplemented
return self.value == other.value
print(UserId(7) == UserId(7))
print(UserId(7) is UserId(7))

When __eq__ returns NotImplemented, Python can try the reflected operation on the other operand before deciding the result. That is why equality is semantic and type-dependent, not a fixed machine-level primitive.

The built-in id() returns an integer unique for the object's lifetime. In CPython this is typically the memory address, but that address interpretation is not a language guarantee.

Aliasing and mutation

Assignment copies references, not objects. If two names alias the same mutable object, mutation through either name is visible through both.

# [CURRENT - 3.10-3.14] Works on Python 3.x
profile = {"name": "Ana", "roles": ["admin"]}
alias = profile
alias["roles"].append("billing")
print(profile["roles"]) # ['admin', 'billing']

This is the object model at work. The mistake is relying on shared mutation without deciding whether it is intentional.

Version context

The identity/equality distinction is stable across Python 3. Current project guidance targets Python 3.10-3.14. Python 3.9 and below are End-of-Life.

CPython-specific interning optimizations for some strings and integers may change when identities happen to coincide for equal immutable values. Those are implementation optimizations, not correctness rules.

Edge cases and gotchas

Use is None, not == None. None is a singleton, and equality can be overloaded in surprising ways.

# [CURRENT - 3.10-3.14] Works on Python 3.x
_missing = object()
def read_config(key, default=_missing):
if default is _missing:
return f"load {key} from environment"
if default is None:
return "explicitly disabled"
return default

Do not compare strings, numbers, lists, or other ordinary values with is just because it sometimes "works" in a REPL. That usually means you accidentally hit an optimization.

If a function receives a mutable object, the contract must say whether it mutates the caller-owned object, snapshots it, or returns a derived copy. Hidden aliasing bugs scale badly in production.

Production usage

Use identity checks for:

  • None
  • sentinel objects
  • rare cases where true object identity is part of the contract

Use equality for business values. Copy at boundaries when the callee should not share ownership of a mutable input.

# [CURRENT - 3.10-3.14] Works on Python 3.x
def normalize_roles(roles):
owned = list(roles)
owned.sort()
return owned
original = ["billing", "admin"]
print(normalize_roles(original))
print(original)

For shared-state bugs caused by reused defaults, see .

Further depth
  • Data model: objects, values, and types
  • Built-in function: id
  • Language reference: comparisons
BOARD NOTESContext
WHY NO BENCHMARK?

This topic is better taught with structure, semantics, and cross-references than with a synthetic chart.

Names are sticky notes, not boxes.

RELATED GUIDES
NEXT CHECKS
Contribute