Python Variables and References
- Description: Name binding semantics, multiple assignment, unpacking, LEGB scope,
global/nonlocal, the mutable-default trap, augmented assignment, andcopy/deepcopy - My Notion Note ID: K2A-D1-2
- Created: 2022-03-05
- Updated: 2026-05-11
- License: Reuse is very welcome. Please credit Yu Zhang and link back to the original on yuzhang.io
Table of Contents
- 1. Names Bind to Objects
- 2. Mutable vs Immutable
- 3. Multiple and Chained Assignment
- 4. Tuple Unpacking
- 5. LEGB Scope Rule
- 6.
globalandnonlocal - 7. Mutable Default Argument Trap
- 8. Augmented Assignment
- 9.
deland Reference Counting - 10.
copyanddeepcopy
1. Names Bind to Objects
- Assignment binds a name to an object, no storage allocation at the name's site, no value copy
- Closer to copying a pointer than to C++
auto b = a;(which would copy forstd::vector)
a = [1, 2, 3]
b = a # b binds to the SAME list
b.append(4)
print(a) # [1, 2, 3, 4]
print(a is b) # True
To get a real copy:
b = a.copy()orb = a[:], shallowb = copy.deepcopy(a), recursive
2. Mutable vs Immutable
| Mutable | Immutable |
|---|---|
list, dict, set, bytearray, most custom classes |
int, float, bool, str, tuple, frozenset, bytes |
- Rebinding an immutable name does NOT mutate, creates a new object and rebinds
- Mutable objects mutate in place under the same syntax
s = "hello"
s += " world" # new str object; old "hello" is unreferenced
n = 5
n += 1 # new int object
xs = [1, 2]
xs += [3] # mutates xs (calls __iadd__); same object
- Asymmetry only matters when the object is shared (function args, closures, defaults)
3. Multiple and Chained Assignment
x, y, z = 1, 2, 3 # tuple unpacking on RHS
a = b = c = 0 # chained: all three bound to the SAME 0
a, b = b, a # swap (no temp; RHS evaluated first)
- Chained assignment binds ONE object to multiple names, matters for mutables
matrix = [[0] * 3] * 3 # all three rows are the SAME list!
matrix[0][0] = 1
print(matrix) # [[1,0,0], [1,0,0], [1,0,0]]
matrix = [[0] * 3 for _ in range(3)] # correct: three distinct lists
4. Tuple Unpacking
a, b = (1, 2)
a, b, c = "xyz" # any iterable
a, *rest = [1, 2, 3, 4] # rest = [2, 3, 4]
first, *middle, last = [1, 2, 3, 4, 5] # middle = [2, 3, 4]
*head, last = "abcd" # head = ['a','b','c'], last = 'd'
Common use:
for i, value in enumerate(items): ...
for key, value in d.items(): ...
- Star-unpacking is also used at call sites to forward arguments
5. LEGB Scope Rule
Name resolution order:
- Local, current function
- Enclosing, outer functions (for nested defs)
- Global, module-level
- Built-in,
len,print, etc.
x = "global"
def outer():
x = "enclosing"
def inner():
print(x) # found in Enclosing → "enclosing"
inner()
outer()
- No block scope: names from
if/forare visible for the rest of the enclosing function (opposite of C++):
def f(flag):
if flag:
msg = "yes"
return msg # UnboundLocalError if flag is False
6. global and nonlocal
- Assigning to a name inside a function creates a local binding by default
- To rebind an outer name, declare it
- Reading an outer name doesn't need a declaration, only rebinding does
counter = 0
def bump():
global counter
counter += 1
def make_counter():
n = 0
def bump():
nonlocal n # rebind in enclosing function
n += 1
return n
return bump
global→ module scopenonlocal(3.0+) → nearest enclosing function scope
7. Mutable Default Argument Trap
- Defaults are evaluated once at
deftime and shared across calls - A mutable default accumulates state
def append_to(item, target=[]): # BUG
target.append(item)
return target
append_to(1) # [1]
append_to(2) # [1, 2], the same list!
append_to(3) # [1, 2, 3]
Fix, use None sentinel:
def append_to(item, target=None):
if target is None:
target = []
target.append(item)
return target
- Applies to any mutable default (
[],{},set(), custom mutable objects) - Immutable defaults (
int,str,tuple) are safe, rebinding doesn't mutate
8. Augmented Assignment
+=,-=,*=,/=,//=,%=,**=,&=,|=,^=,<<=,>>=,@=- Each calls the in-place dunder (
__iadd__, etc.) when defined; falls back to the binary op
xs = [1, 2]
ys = xs
xs += [3] # in-place: xs and ys both [1, 2, 3]
xs = xs + [4] # new list: ys still [1, 2, 3], xs is [1, 2, 3, 4]
- Equivalent in effect for immutable types
- Different for mutables,
__iadd__mutates and may also rebind LHS
9. del and Reference Counting
a = [1, 2, 3]
b = a
del a # b still holds the list; alive
del b # refcount → 0; freed
del nameunbinds a name- Object is freed when refcount hits zero (CPython), or by the cyclic GC for reference cycles
- No
deleteoperator like C++, deterministic cleanup useswith(context managers)
10. copy and deepcopy
copy.copy(x), shallow (top-level container is new, nested objects shared)copy.deepcopy(x), recursive
import copy
a = [[1, 2], [3, 4]]
b = copy.copy(a)
b[0].append(99)
print(a) # [[1, 2, 99], [3, 4]] , inner list shared
c = copy.deepcopy(a)
c[0].append(7)
print(a) # unchanged
Quick shallow copies:
list_copy = my_list[:] # or my_list.copy()
dict_copy = dict(my_dict) # or my_dict.copy()
set_copy = set(my_set) # or my_set.copy()
- For value-type semantics across a whole class, implement
__copy__and__deepcopy__