Python
You Think You Know Python Variables? Here's How Memory Really Works
Python variables aren't boxes—they're references. Learn how memory actually works with id(), aliasing, mutability, and garbage collection to avoid silent bugs.
June 2026 · 8 min read · 1 views · 0 hearts
Advertisement
You Think You Know Python Variables? Here's How Memory Really Works
Most Python tutorials tell you variables are "labels for data." That's technically true — but it hides the absolute weirdest, most powerful part of how Python actually stores things in memory.
Let’s pop the hood.
Variables Are Not Boxes
If you learned programming with C or Java, you probably think of a variable as a bucket you put a value into. Like:
x = 5 # put 5 in the box labeled x
But in Python, variables don't hold values. They reference objects.
Think of it like a name tag pinned on a real object. The object (say, the integer 5) lives somewhere in memory. The variable x is just a sticky note pointing to it.
This distinction changes everything.
The id() Trick
Every Python object has a unique identity (like a memory address). You can check it with id():
x = 5
y = 5
print(id(x)) # 140711111111040
print(id(y)) # 140711111111040 — same!
Wait — x and y point to the same object? Yes. Python caches small integers (-5 to 256) and reuses them for performance. This is called interning.
But try this:
a = 257
b = 257
print(id(a), id(b)) # Different addresses!
Now Python creates two separate objects. For large integers, it doesn't bother caching.
The "Aliasing" Trap
This reference model hits you hardest with mutable objects — lists, dicts, sets.
original_list = [1, 2, 3]
alias = original_list # No copy! Just another name tag
alias.append(4)
print(original_list) # [1, 2, 3, 4] — surprise!
alias and original_list point to the same list object. Changing one changes the "other" — because there is no other.
How to actually copy:
import copy
shallow_copy = original_list[:] # works for simple lists
deep_copy = copy.deepcopy(original_list) # for nested structures
Mutability vs Immutability
Immutable objects (int, float, str, tuple, frozenset) can't be changed in place. When you "modify" a string, Python actually creates a new one:
s = "hello"
t = s.upper() # Creates NEW string object
print(s) # "hello" — unchanged
Mutable objects (list, dict, set, bytearray) can be changed. That's why two variables pointing to the same list is dangerous — they share the mutable object.
The Reference Counting Game
Python tracks how many references point to each object. When the count hits zero, the object gets garbage-collected.
x = [1, 2, 3] # reference count = 1
y = x # reference count = 2
del y # reference count = 1
del x # reference count = 0 → garbage collected
You can check yourself with sys.getrefcount() (but note: passing it to the function temporarily bumps the count by 1):
import sys
z = [1, 2, 3]
print(sys.getrefcount(z)) # 2 (the variable + the function argument)
Circular References and the Garbage Collector
What happens when two objects reference each other?
a = []
b = [a]
a.append(b) # Now a contains b, b contains a
del a
del b # Reference counts never reach zero!
Python's cyclic garbage collector handles this. It periodically scans for groups of objects that reference each other but have no external references — and cleans them up. You rarely need to think about it, but gc.collect() can force a sweep in tight memory loops.
The is vs == Headache
==checks value equality ("do these objects have the same contents?")ischecks identity equality ("do these variables point to the same object?")
x = [1, 2, 3]
y = [1, 2, 3]
print(x == y) # True — same values
print(x is y) # False — different objects in memory
Always use is for None comparisons, and == for everything else.
Memory for Function Arguments
When you pass an argument to a function, Python passes the reference, not a copy. This is often called "pass by object reference."
def modify(lst):
lst.append(4)
my_list = [1, 2, 3]
modify(my_list)
print(my_list) # [1, 2, 3, 4] — modified in place
But reassigning inside a function doesn't affect the outer variable:
def reassign(lst):
lst = [10, 20, 30] # Points local name to new object
my_list = [1, 2, 3]
reassign(my_list)
print(my_list) # [1, 2, 3] — unchanged
The local variable lst now points to a new list, but the outer my_list still points to the original.
The Takeaway
Understanding Python's memory model isn't academic trivia — it's the difference between bugs you chase for hours and clean, predictable code.
| Concept | Problem It Solves |
|---|---|
| Variables are references | Avoid accidental mutation with aliases |
id() and is |
Debug variable identity |
| Immutability | Safe sharing across functions |
| Reference counting + GC | Memory management understanding |
Next time you write a = b, ask yourself: Am I making an alias, or do I need a copy? That question alone will save you more headaches than any linting tool.
Advertisement
Comments
Questions, corrections, and tips stay visible for everyone reading this page.
Join the discussion
No comments yet
Be the first to leave a note — it helps the next reader.