String interning and the small-int cache
CPython optimizes some immutable values by interning them — keeping a single canonical instance and reusing it. Knowing what’s interned vs not is mostly about not making is comparisons that “work in tests but fail in production.”
The small-int cache
Integers in [-5, 256] are pre-allocated at interpreter startup. int(5) always returns the same object.
a = 100
b = 100
a is b # True
a = 257
b = 257
a is b # often False (depends on context)
The cutoffs are CPython implementation details. Changing them is a one-line patch that rarely happens.
String interning
Three categories of strings:
-
Always interned — string literals that look like Python identifiers (alphanumeric + underscore), short enough, computed at compile time.
"hello" is "hello" # True "abc_123" is "abc_123" # True -
Sometimes interned — strings the compiler folds together (constant subexpressions in the same code object).
"hello world" is "hello world" # True if both literals in same module -
Never auto-interned — runtime-built strings, strings with whitespace/punctuation, long strings.
("hel" + "lo") is "hello" # often False (depends on optimization) "hello world!" is "hello world!" # often False
Force interning with sys.intern:
import sys
a = sys.intern("computed_at_runtime")
b = sys.intern("computed_at_runtime")
a is b # True
When does interning matter?
Mostly never for correctness — use == for comparisons. It matters for:
- Hash table workloads — interned strings have cached hashes, dict lookups skip rehashing.
- Memory — millions of identical user-input strings can be deduplicated with
sys.intern. - Profiling interned strings as identifiers in hot dict-key positions speeds things up measurably.
Real example: ORM row dicts
If you fetch 1M rows and build dicts with column names as keys, every dict has its own copy of those keys. Interning reduces memory:
import sys
cols = [sys.intern(c) for c in cursor.description]
rows = [dict(zip(cols, row)) for row in cursor.fetchall()]
Now all dicts share the same key objects.
What about other types?
bool—TrueandFalseare singletons (alwaysTrue is True).None— singleton (x is Noneis the canonical idiom).frozenset,tuple,bytes— not interned by default. Equal tuples are not necessarily the same object.- Empty containers (
(),frozenset()) — some are cached for efficiency. Don’t rely on it.
The takeaway
Use is for: None, True, False, sentinels, explicit identity checks.
Use == for everything else.
Don’t rely on is returning True for “small ints” or “short strings” in production code, even if it works in your REPL.
See also tricky_questions/03_is_vs_equals_interning.md.
Interview angle
“Why does 1 is 1 return True but 1000 is 1000 sometimes False?” (Small-int cache.) “How would you reduce memory of millions of dicts with the same keys?” (sys.intern the keys.)