🧠 Python's Memory Model: The Ghosts You Can't Escape

📅 5 July, 2025 ⏱️ 15 minute read

⚙️ Python's Identity Crisis: When is Isn't

Python sometimes behaves like it has a memory mind of its own. Consider this:

a = 256
b = 256
print(a is b)  # True

a = 257
b = 257
print(a is b)  # False

Weird, right? Why would two identical-looking numbers behave differently? Well, it turns out Python (more specifically, CPython) does some sneaky optimizations to save memory and boost performance. The magic behind this behavior comes down to three things: immutability, interning, and caching.

🧋 Immutability: The Foundation of Borrowing

Immutable objects are those whose value cannot change after creation. Because of this property, Python can safely reuse immutable objects without worrying about unintended side effects.

Common immutable types:

  • int, float, bool
  • str
  • tuple (if all elements are also immutable)

The big takeaway here is: if two immutable objects hold the same value, Python can just point both variables to the same place in memory. Pretty neat.

This makes Python way more beginner-friendly compared to lower-level languages like C or Rust, where memory is your responsibility and every mistake costs you sanity (and your precious weekend).

🧵 Interning: Cause Why Pay Rent Twice?

🔹 What Is Interning?

Interning is a technique where Python stores only one copy of certain immutable values. So when you create another identical object, Python just reuses the existing one. This helps save memory and speeds up comparisons with is.

String Interning: Be Short, Be ASCII, BeLoved

Python automatically interns some strings - typically those that are:

  • Short
  • ASCII-only
  • Static literals (defined directly in code)
a = "python"
b = "python"
print(a is b)  # True (interned)

But strings created at runtime often aren't interned:

a = "py" + "thon"
b = "python"
print(a is b)  # True (optimizer folded literals)

a = "".join(["py", "thon"])
print(a is b)  # False (created at runtime)

You can force interning manually using sys.intern():

import sys
a = sys.intern("py" + "thon")
b = "python"
print(a is b)  # True

Under the hood, CPython keeps a global table of interned strings. When you use sys.intern(), it looks up this table and ensures your string shares memory with an existing copy if available.

🔢 Integer Caching

This one's got some history. CPython preloads all integers from -5 to 256 during startup and stashes them in a cache. Why? Because these numbers show up all the time - in loops, conditions, indexing, etc.

a = 256
b = 256
print(a is b)  # True (same object from cache)

a = 257
b = 257
print(a is b)  # False (different objects)

You can confirm this using id() - the memory address for 256 stays the same; for 257, it changes like your motivation after debugging for 10 minutes.

print(id(256))  # same ID every time
print(id(257))  # different ID on each assignment

In CPython, this is implemented in Objects/longobject.c using a preallocated array for small integers.

🧪 is vs ==: One Checks Your Soul, The Other Your Face

It's important to understand the difference between identity and equality:

  • == checks value equality
  • is checks identity (i.e., same memory address)
a = "hello"
b = "hello"
print(a == b)  # True
print(a is b)  # True (maybe interned)

a = "".join(["he", "llo"])
print(a == b)  # True
print(a is b)  # False

Use == unless you're explicitly checking for identity - e.g., with singletons like None, True, or False.

⚡ Real-World Examples

🧠 Dictionary Keys: The VIPs of Interning Club

Because dictionary keys must be hashable and often strings or small ints, Python's interning improves dict performance:

my_dict = {"status": "ok", 1: "one"}

The key "status" and the integer 1 are likely interned and reused.

📦 Immutable Tuples

Tuples containing only immutable objects can be reused. However, Python doesn't automatically intern them:

a = (1, 2, 3)
b = (1, 2, 3)
print(a is b)  # False (usually)

If you want identity reuse for immutable collections, consider memo-ization strategies like functools.lru_cache.

🚀 Conclusion

Understanding how Python reuses memory lets you write faster, cleaner code - and maybe even fix that one bug that only happens “sometimes.” These memory quirks might seem harmless, but they can lead to sneaky bugs if you trust is too much. Respect identity, and maybe - just maybe - your code will behave itself for once.