Coming from other languages, it's easy to think of a variable as a labeled box holding a value. In Python, it's more accurate to think of a variable as a name pointing to an object living somewhere in memory. That distinction explains a lot of Python's behavior.
Variables Are References
x = [1, 2, 3]
y = x # y points to the SAME list as x
y.append(4)
print(x) # [1, 2, 3, 4] — x changed too!
x and y aren't separate copies — they're two names pointing at the same object. Assignment in Python never copies data; it just binds a name to an object.
Mutable vs Immutable Matters Here
This "shared reference" behavior only causes surprises with mutable objects (lists, dicts, sets). With immutable objects (int, str, tuple, float), you can't accidentally mutate shared state, because there's no way to change the object in place:
a = 5
b = a
b += 1
print(a) # 5 — unaffected, because b += 1 creates a NEW int object
Dynamic Typing
Python variables don't have a fixed type — they can be rebound to any kind of object at any time:
value = 42
value = "now I'm a string"
value = [1, 2, 3]
The object has a type; the variable is just a label that can point to any object.
Scope: Where a Variable Lives
Python resolves names using the LEGB rule — Local, Enclosing, Global, Built-in:
x = "global"
def outer():
x = "enclosing"
def inner():
x = "local"
print(x) # "local"
inner()
print(x) # "enclosing"
outer()
print(x) # "global"
Each level of nesting can shadow the name from an outer scope without affecting it.
global and nonlocal
By default, assigning to a variable inside a function creates a new local variable rather than modifying an outer one:
counter = 0
def increment():
counter += 1 # UnboundLocalError! Python sees this as a local assignment
def increment_fixed():
global counter
counter += 1 # now this modifies the global variable
nonlocal does the same thing for enclosing (not global) scope, commonly used in closures.
A Common Pitfall: Mutable Default Arguments
def add_item(item, basket=[]): # DANGER
basket.append(item)
return basket
print(add_item("apple")) # ['apple']
print(add_item("banana")) # ['apple', 'banana'] — the SAME list persists!
Default argument values are evaluated once, when the function is defined — not each time it's called. The fix is to use None as a sentinel:
def add_item(item, basket=None):
if basket is None:
basket = []
basket.append(item)
return basket
Key Takeaway
Python variables are names bound to objects, not containers holding values. Once that model clicks, behaviors that seem confusing at first — shared references, mutable defaults, scope shadowing — become predictable consequences of a single consistent rule.
Comments (0)
No comments yet
Be the first to share your thoughts!