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.

:)
Python version

Targets Python 3.10–3.14. Python 3.9 and below are End-of-Life.

TABLE OF CONTENTS
2.1Identity, equality and aliases

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

Identity and equality answer different questions. is asks whether two references point at the same object. == asks whether the operands agree with an equality protocol that may run arbitrary Python code.

Core answer

Use identity for sentinels and singleton checks such as value is None. Use equality for value contracts. Then decide whether aliasing is acceptable before mutating an object shared through multiple names.

# [CURRENT - 3.10-3.14] Works on Python 3.10+
from dataclasses import dataclass
@dataclass(frozen=True, slots=True)
class AccountId:
raw: str
def same_account(left: AccountId, right: AccountId) -> bool:
return left == right
first = AccountId("ACCT-42")
second = AccountId("ACCT-42")
alias = first
print(same_account(first, second))
print(first is second)
print(first is alias)

Why this design exists

Python names are references to objects, not value slots that copy every assignment. Identity gives the runtime and user code a cheap way to ask whether two references share one object. Equality is overloadable because domains need value semantics: two money records, normalized paths, or account identifiers may compare equal without being one allocation.

The separation keeps sentinel APIs reliable. A user-defined __eq__ can be slow, surprising, or intentionally broad; it cannot change what is means.

Mechanics and CPython internals

At the CPython level, variables hold references to PyObject instances. Assignment increments reference relationships; it does not clone the object. Reference counting and cyclic GC decide lifetime, while equality dispatch follows the data model and can call __eq__ on one or both operands. CPython may intern or cache some objects, but those optimizations are not a portable basis for business logic.

# [CURRENT - 3.10-3.14] Works on Python 3.10+
from dataclasses import dataclass, field
@dataclass(slots=True)
class RetryState:
order_id: str
attempts: list[str] = field(default_factory=list)
def record_retry(state: RetryState, reason: str) -> None:
state.attempts.append(reason)
primary = RetryState("ORD-9")
shared = primary
record_retry(shared, "timeout")
print(primary.attempts)
print(primary is shared)
print(primary == shared)

Complexity and tradeoffs

Identity comparison is constant-time pointer comparison. Equality cost is whatever the operands implement: a frozen identifier may compare in O(1), while a nested list comparison can scan until a mismatch or consume O(n) work. Value semantics improve API clarity; identity semantics expose aliasing and sentinel state without delegating to user code.

Idiomatic patterns and refactoring

Prefer explicit sentinel identity when None is a valid domain value. That keeps the function signature honest and avoids equality dispatch on user input.

# [CURRENT - 3.10-3.14] Works on Python 3.10+
from dataclasses import dataclass
from typing import Final
@dataclass(frozen=True, slots=True)
class PricingRule:
name: str
discount: float | None
_MISSING: Final = object()
def discount_or_default(rule: PricingRule, value: object = _MISSING) -> float | None:
if value is _MISSING:
return rule.discount
if value is None or isinstance(value, float):
return value
raise TypeError("discount override must be float or None")
rule = PricingRule("new-customer", None)
print(discount_or_default(rule))
print(discount_or_default(rule, 0.10))

Common mistakes and edge cases

Do not use is for strings, integers, or dataclass values because an interpreter cache made a local experiment look stable. Do not assume equality is side-effect free or cheap for arbitrary user types. Do not shallow-copy a container and then expect nested mutable state to stop aliasing.

When to use / When NOT to use

Use is for identity questions, singletons, and sentinels. Use == when a type advertises a value contract and the code wants that contract.

Do not turn identity into a performance micro-optimization for normal value comparison, and do not use equality when the protocol itself is the thing you need to avoid.

Further reading

  • Official docs: data model value comparisons
  • Official docs: object model
  • PEP 683: immortal objects in CPython
  • CPython source: Objects/object.c
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