Mastery
Mastery/Python/A. Object model, identity & memory
T1 · high-leverage
Compare with Java: Identity vs. equality, and reference caching

is vs ==, the small-int cache, and string interning

  • == compares value (calls __eq__).
  • is compares identity (same object in memory, same id()).

There are two completely different mechanisms that can make is return True for equal-looking literals, and mixing them up is why "small int caching" advice online is so inconsistently reproducible. This notebook demonstrates both, and — importantly — it's being run in Jupyter/IPython, which compiles each top-level statement in a cell separately. A plain .py script compiles its whole file as one code object instead, so a couple of these results genuinely come out different in a script vs. here. That difference is real and verified below, not a caveat to wave away.

Mechanism 1: the small-int cache (-5..256) -- a genuine runtime singleton, same everywhere

python
# Python 3.8+ warns on `is` against an int/str literal, since it's almost always a mistake.
# We're deliberately doing it on purpose here to demonstrate identity semantics, so silence that warning.
import warnings
warnings.filterwarnings("ignore", category=SyntaxWarning)

a = 256
b = 256
print("256 is 256:", a is b, "|", a == b)

e = -5
f = -5
print("-5 is -5: ", e is f, "  <- both in the cached range, always True, script or notebook")

Mechanism 2: compile-time constant folding -- only within ONE code object

Outside the cache, is can still return True for equal integer literals, but only because the compiler deduplicated them into one shared constant within a single code object. Two literals in the same statement/expression always share a code object, so this folds regardless of environment:

python
print("within ONE statement:", 257 is 257, "  <- both literals compiled together, folded, always True")

But two literals in separate statements only share a code object if the whole surrounding block was compiled as one unit. A .py script's top-level statements ARE one unit (verified separately, outside this notebook, via python3 script.py: c = 257; d = 257 on two lines printed True). Jupyter/IPython compiles each top-level statement in a cell on its own, so that sharing does NOT happen here -- watch this actually run, in this exact notebook:

python
c = 257
d = 257
print("257 is 257, separate statements, IN THIS NOTEBOOK:", c is d, "  <- False here; a .py script would print True")

g = -6
h = -6
print("-6 is -6, separate statements, IN THIS NOTEBOOK:  ", g is h, "  <- outside the cache AND no folding across statements")

eval() always compiles a brand new, separate code object no matter what -- never shares constants with anything, in a script or a notebook:

python
x = 257
y = eval("257")
print("257 vs eval('257'):", x is y)

Mechanism 3: automatic interning of identifier-shaped string literals -- also a runtime thing, same everywhere

CPython automatically interns string literals that look like an identifier (letters/digits/underscore only, no spaces) as a genuine runtime optimization -- independent of which code object compiled them. This is why "hello" behaves differently from "hello world" even across separate statements, in ANY environment:

python
s1 = "hello"
s2 = "hello"
print("'hello' (identifier-shaped), separate statements:      ", s1 is s2, " <- real auto-interning, not folding")

s5 = "hello_world"
s6 = "hello_world"
print("'hello_world' (identifier-shaped), separate statements:", s5 is s6, " <- also auto-interned")

s3 = "hello world"
s4 = "hello world"
print("'hello world' (has a space), separate statements, HERE:", s3 is s4, " <- False here; NOT auto-interned, and no cross-statement folding in a notebook (a script would print True)")

s7 = "".join(["hel", "lo"])
print("runtime-built 'hello' vs the literal:                  ", s7 is s1, "| but equal:", s7 == s1, " <- interning never applies to strings built at runtime")

Takeaway: never rely on is for value equality. Even setting that aside, "is this cached/interned" has three independent answers depending on which mechanism applies (runtime singleton cache, compile-time folding within one code object, or runtime auto-interning of identifier-shaped strings) -- and the compile-time one genuinely gives different results in a script vs. a notebook, which is exactly the kind of thing that makes is-based micro-optimizations unreliable across environments.

Interview angle

This is a classic gotcha question precisely because it's easy to get partially right — most candidates know "the cache is -5 to 256" as a memorized rule, without understanding why, which is exactly what a good interviewer probes with a follow-up like "would that still be true inside a function, or in the REPL?" A strong answer explains the mechanism (a real runtime singleton cache vs. compiler constant-folding vs. string auto-interning are three different things), not just the observed behavior. That distinction is also what lets someone predict new edge cases instead of having memorized one.

In the industry

In real code, nobody uses is to compare ints or strings for equality on purpose — linters catch it outright (ruff's F632, ILP32-style checks in most IDEs) and code review would flag it immediately. The one legitimate, idiomatic use of is in production Python is comparing against singletons you control: is None, is True, or your own sentinel objects and Enum members. String interning specifically matters in performance-sensitive code that handles huge volumes of repeated short strings (tokenizers, parsers) where an explicit sys.intern() call is sometimes used deliberately as a memory optimization.