backend / python core / 30_string_interning.md

String interning and the small-int cache

2 min read source

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:

  1. 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
  2. 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
  3. 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?

  • boolTrue and False are singletons (always True is True).
  • None — singleton (x is None is 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.)