03_Understanding Variables: Labels vs. Boxes Explained

If you are coming from languages like C++ or Java, Python variables are probably gaslighting you. They look the same, they use the same = syntax, but they behave completely differently under the hood.
I realized I had the wrong mental model. In Python, variables aren't boxes. They are sticky notes.
In this guide, let’s explore what actually happens in memory when you create a variable, and why the "Box vs. Label" distinction is the most important concept in Python.
1. The Fundamental Truth: Labels, Not Boxes
In statically typed languages (like C), a variable is a specific chunk of memory reserved for a specific type of data. It’s a literal container.
In Python, variables are just labels (or name tags) that point to objects living somewhere in the heap memory.
The Visualization
When you write x = 5, you aren't putting "5" into a box named x.
Python creates an integer object
5somewhere in memory.It writes "x" on a string (a reference) and ties it to that object.
x = 5 # x is a label attached to the object 5
The Proof: We can use the id() function (which reveals the memory address) to prove this.
x = 42
print(f"Address of x: {id(x)}")
# Output: 14071... (Some memory address)
If you reassign x = 43, you aren't changing the value inside the box. You are ripping the "x" tag off the 42 object and sticking it onto a totally new object 43 at a different address.
2. Assignment = Aliasing (The "Two Tags" Rule)
Because variables are just references, assignment in Python behaves uniquely. This is where most beginners get bitten by bugs.
When you do reference = original, you are not copying the data. You are simply sticking a second label onto the same existing object.
original = [1, 2, 3]
reference = original # Both now point to the same list
reference.append(4) # Modifying one affects the other
print(original)
# Output: [1, 2, 3, 4] -> It changed!
My Takeaway: If I want a true independent copy, I have to explicitly ask for one using original.copy() or slicing original[:].
3. Dynamic Typing & "Duck Typing"
Since variables are just labels, they don't care what they are attached to. A label can point to a cat one second and a dog the next.
x = 10 # x labels an integer
x = "Hello" # x now labels a string
This leads to Python's famous philosophy: Duck Typing. "If it walks like a duck and quacks like a duck, it must be a duck."
You don't need to specify that a function accepts a File object. As long as the object passed has a .read() method, Python is happy. This allows for incredible flexibility and rapid prototyping, but requires discipline to keep track of what your variables actually hold.
4. Power Feature: Unpacking
One of the coolest features I found is Extended Unpacking. It allows you to grab specific elements from a list using the asterisk * operator.
# Swapping without a temporary variable (The Pythonic Way)
a, b = 10, 20
a, b = b, a # Swaps values instantly
# Grabbing the middle of a list
first, *middle, last = [1, 2, 3, 4, 5]
print(first) # 1
print(middle) # [2, 3, 4]
print(last) # 5
5. Scope: The LEGB Rule
Understanding where a variable lives is crucial to avoiding bugs. Python resolves names using the LEGB Rule, searching scopes in this specific order:
Local (Inside the current function)
Enclosing (Inside enclosing functions/nested functions)
Global (Module level)
Built-in (Python's keywords like
listorstr)
The Common Pitfall: If you want to modify a global variable inside a function, you must use the global keyword. Otherwise, Python just creates a new local variable with the same name, leaving the global one untouched.
counter = 0
def update():
global counter # Tell Python we mean the global one
counter += 1
6. A Reminder on Mutability
As we discussed in the previous post, this "Label" system explains the infamous Mutable Default Argument trap.
If you use a list as a default argument, that list is created once when the function is defined. The label stays attached to that same list forever.
❌ The Wrong Way:
def add_item(item, items=[]):
items.append(item)
return items
# This list will persist and grow across different function calls!
✅ The Correct Way:
def add_item(item, items=None):
if items is None:
items = [] # Create a new list every time
items.append(item)
return items
Conclusion: PEP 8 and Best Practices
To wrap up, treating variables as labels rather than boxes unlocked a deeper understanding of Python for me. To keep things clean, I try to follow PEP 8 naming conventions:
Variables/Functions:
snake_case(e.g.,user_name)Constants:
SCREAMING_SNAKE_CASE(e.g.,MAX_RETRIES)Classes:
PascalCase(e.g.,UserAccount)Private Members:
_leading_underscore(e.g.,_internal_value)



