Skip to content

Identity, is vs ==, & Interning Interview Questions & Answers

15 questions Updated 2026-06-18 Share:

Python interview questions on object identity: is vs ==, the id() function, the small-int cache, string interning, and the correct use of is for None and sentinels.

Read the in-depth guidePython Identity Explained — is vs ==, id(), and Integer/String Interning(opens in new tab)
15 of 15

== tests value equality — "do these represent the same data?" — by calling the object's __eq__. is tests identity — "are these the exact same object in memory?" — which is effectively an id() comparison. They frequently agree, but conceptually they ask completely different questions.

a = [1, 2, 3]
b = [1, 2, 3]
a == b      # True  — equal contents
a is b      # False — two distinct list objects

c = a
c is a      # True  — same object, just another name

Use == whenever you care about the value, which is almost always. Reserve is for identity checks against singletons. Rule of thumb: if you're comparing data, use ==; if you're checking "is this literally that object," use is.

id(obj) returns a unique integer identity for an object that's constant for the object's lifetime. In CPython it's the object's memory address, though that's an implementation detail. Two names with the same id refer to the same object, and is is essentially an id comparison.

a = [1, 2]
b = a
id(a) == id(b)   # True  — same object
a is b           # True  — equivalent check

c = [1, 2]
id(a) == id(c)   # False — equal value, different object

id is handy for understanding aliasing — why mutating through one name shows up through another. Don't rely on the actual numeric value (it can be reused after an object is garbage-collected); use it only to reason about sameness.

CPython pre-creates and caches integers from −5 to 256 as singletons at startup. Any time a value in that range is needed, the same cached object is reused — so equal small ints share identity and is returns True. Outside that window, equal integers are typically distinct objects.

a = 256
b = 256
a is b      # True  — both point at the cached 256

c = 257
d = 257
c is d      # often False — separate objects
c == d      # True  — always compare values with ==

This is purely a memory/performance optimization and a CPython implementation detail — not a language guarantee. It's the single biggest reason is "appears" to work on numbers. Rule of thumb: never use is to compare ints, use ==.

Interning stores a single shared copy of a string so identical strings can be the same object, saving memory and making equality checks a fast pointer comparison. CPython auto-interns short, identifier-like string literals (and compile-time constants); other strings may not be. You can force it with sys.intern.

a = "hello"
b = "hello"
a is b           # True  — auto-interned literal

c = "hello world!"
d = "hello world!"
c is d           # often False — not auto-interned

import sys
e = sys.intern("hello world!")
f = sys.intern("hello world!")
e is f           # True — explicitly interned

sys.intern is useful when you compare the same strings repeatedly (parsing, tokenizing, dict keys) and want the speed/memory win. Like int caching, which strings auto-intern is an implementation detail — never rely on it for correctness.

is sometimes returns True for equal values only because CPython caches or interns those particular objects — small ints (−5..256) and many short string literals share one object. It's an accident of optimization, not equality, so it breaks the moment you leave the cached range.

x = 100
x is 100         # True  — cached small int (deceiving!)

y = 1000
y is 1000        # often False — outside the cache
y == 1000        # True  — the correct comparison

The danger is that code passes during testing with small values and then fails in production with larger ones. Treat any is-on-a-value that works as a lucky coincidence. Rule of thumb: if swapping is for == would change behavior on some input, you should have used ==.

Use is to test identity against singletons — objects of which there is exactly one — most commonly None, but also True, False, and your own sentinel objects. For singletons is is correct, fast, and can't be fooled by a custom __eq__.

if x is None:          # idiomatic, recommended by PEP 8
    ...

_MISSING = object()    # unique sentinel
def get(d, key, default=_MISSING):
    val = d.get(key, _MISSING)
    if val is _MISSING:    # distinguishes "absent" from "value is None"
        return default
    return val

A sentinel like object() is ideal precisely because is checks identity — no other object can ever match it. Rule of thumb: is/is not for None and sentinels; ==/!= for everything else.

Dicts/sets find items by hash first, then == (not is). Two objects that are == and have equal hashes are treated as the same key. Identity only matters as an optimization: containers may short-circuit x is key before calling __eq__.

a = (1, 2)
b = (1, 2)
a is b              # False — distinct objects
d = {a: "x"}
d[b]                # 'x' — found by hash + ==, identity irrelevant

# nan is the exception: nan != nan, but `in` checks identity first
n = float("nan")
n in [n]            # True — same object short-circuits the == check

Rule of thumb: dict/set membership uses hash + ==, so equal-and-hashable objects are interchangeable keys; the nan-in-list quirk comes from the identity shortcut.

No — id() is guaranteed constant for the object's lifetime. But once an object is garbage-collected, CPython may reuse that id for a new object, so comparing ids of objects with non-overlapping lifetimes is meaningless.

id(object())        # some address
id(object())        # MAY be the same! first object was already freed

x = object(); old = id(x)
id(x) == old        # True — stable while x is alive

Rule of thumb: id is stable per live object but can be recycled after collection — never store an id to identify an object across its lifetime; keep a real reference.

They are true singletons — the interpreter guarantees exactly one instance of each. So is None, is True, is False are always correct and can't be subverted by a custom __eq__. PEP 8 mandates is for these.

x is None           # the canonical, recommended check
x is True           # exact identity (rarely needed; usually just `if x:`)

class Tricky:
    def __eq__(self, other): return True
Tricky() == None    # True — misleading!
Tricky() is None    # False — identity can't be fooled

Rule of thumb: always use is/is not with None (and the bool singletons); == can be overridden and lie, is cannot.

The compiler stores immutable literals in co_consts and may share one object for identical constants within the same code unit (constant folding/deduplication). This makes is accidentally True — a compile-time artifact, not a guarantee.

a = (1, 2, 3)
b = (1, 2, 3)
a is b              # may be True at module level (shared constant), False elsewhere

x = "long string"; y = "long string"
x is y              # may be True if folded into one constant

Rule of thumb: literal sharing depends on compilation context — never rely on is for tuples/strings; compare with ==.

Use sys.intern when you process many duplicate strings and compare them repeatedly — parsers, tokenizers, large dict keys, dedup of column values. Interning turns equality into a fast pointer check and saves memory by sharing one copy.

import sys
# intern repeated field names while parsing millions of records:
key = sys.intern(raw_key)
record[key] = value
# later comparisons of interned keys are O(1) identity checks

Rule of thumb: intern high-duplication, frequently-compared strings for memory/speed wins; don't bother for unique or short-lived strings.

Equality should be reflexive: x == x must be True (except deliberate cases like nan). Containers rely on this — they often check x is key as a fast path before ==, so a broken __eq__ that fails on itself causes lookups to misbehave.

class Bad:
    def __eq__(self, other): return False   # not reflexive!

b = Bad()
b == b              # False — violates expectations
b in [b]            # still True — saved only by the `is` short-circuit

Rule of thumb: keep __eq__ reflexive and consistent with __hash__; the identity short-circuit in containers assumes an object equals itself.

Assignment (b = a) creates an alias (b is a). A copy (a[:], list(a), copy.copy) makes a new object (b is not a) but shares nested references (shallow). copy.deepcopy makes new objects all the way down.

import copy
a = [[1], [2]]
b = a              # alias
c = a[:]           # shallow copy
d = copy.deepcopy(a)

b is a             # True
c is a             # False — new outer list
c[0] is a[0]       # True  — shared inner list (shallow)
d[0] is a[0]       # False — fully independent

Rule of thumb: = aliases, shallow copy duplicates the outer object but shares innards, deepcopy duplicates everything — choose by how deep your independence must be.

Beyond ints −5..256, CPython caches the empty tuple (), the empty string/ bytes, single-character latin-1 strings, and shares None/True/False/... (Ellipsis). These are reused, so is may return True — again an implementation detail.

() is ()            # True — single empty-tuple singleton
"" is ""            # True — empty string cached
bool(1) is True     # True — bool singletons
... is Ellipsis     # True

[] is []            # False — empty lists are NOT cached (mutable)

Rule of thumb: immutable empties/singletons are often shared (so is may pass), but mutable empties ([], {}) are always fresh — still compare values with ==.

Print id() of suspected variables to confirm whether two names point at the same object. If mutating one unexpectedly changes another, matching ids prove they're aliases (often from a missing copy).

def add_item(item, basket=[]):    # mutable default bug
    basket.append(item)
    return basket

r1 = add_item("a")
r2 = add_item("b")
id(r1) == id(r2)     # True — same list! reveals the shared-default bug

Rule of thumb: equal id()s on values you expected to be independent flag an aliasing/shared-reference bug — fix by copying or using a fresh object.

More ways to practice

The self-quiz is live. Join our channel for updates, new content & tech tips.

Join our WhatsApp Channel