Gotchas & PitfallsLesson 2 of 2
Scope, Closures & Traps
Python scope rules follow LEGB: Local, Enclosing, Global, Built-in. You can READ outer variables freely, but REASSIGNING them requires explicit permission with the global or nonlocal keyword. Without it, Python treats the name as a new local variable, and reading it before assignment raises UnboundLocalError.
This catches JS developers constantly. In JS, count++ inside a function modifies the outer count. In Python, count += 1 without 'global count' fails. The fix is straightforward, but you need to know it exists. Note that mutating an outer object is fine without any keyword: items.append(x) works, because you are not rebinding the name.
Here is the subtle part: if you ASSIGN to a variable anywhere inside a function, Python treats that variable as local to the ENTIRE function, even on lines before the assignment. Reading it before the assignment line raises UnboundLocalError, even if a global with the same name exists. Python decides scope when it compiles the function, not while it runs.
Also remember: Python has no block scope. Variables created inside if, for, while, with, or try blocks live on after the block ends. Only functions, classes, modules, and comprehensions create new scopes. This is the opposite of JS's let and const, which are block-scoped.
The mutable default argument trap is Python's most infamous gotcha. Default argument values are evaluated ONCE, when the function is defined, not each time it is called. So def add_item(item, items=[]) shares the SAME list across all calls. The fix: use None as the default and create a new list inside the function.
Quick reference
| JavaScript | Python | Note |
|---|---|---|
| let x = 0; fn() { x++ } | global x needed | Must declare intent to rebind |
| Closure modifies outer | nonlocal x needed | Explicit for closures too |
| function(x, arr=[]) {} | def fn(x, arr=None): | Never use [] as default! |
| Block scope (let/const) | Function scope only | No block-level scope |
Side by side
// JavaScript scope
let count = 0;
function increment() {
count++; // modifies outer directly
}
function makeCounter() {
let n = 0;
return () => ++n; // closure modifies outer
}
// Default params - fresh each call
function addItem(item, list = []) {
list.push(item);
return list;
}
addItem("a"); // ["a"]
addItem("b"); // ["b"] - fresh []# Python scope
count = 0
def increment():
global count # REQUIRED!
count += 1
def make_counter():
n = 0
def inc():
nonlocal n # REQUIRED for closures!
n += 1
return n
return inc
# TRAP: Mutable default argument
def add_item(item, items=[]): # BUG!
items.append(item)
return items
add_item("a") # ["a"]
add_item("b") # ["a", "b"] SHARED!
# Fix:
def add_item(item, items=None):
if items is None:
items = []
items.append(item)
return itemsExercise
Real Python, running in your browserFix the 3 bugs related to scope and mutable defaults:
total = 0
def add_to_total(n):
total += n
return total
def make_multiplier(factor):
def multiply(x):
factor = factor * 1 # "normalize"
return x * factor
return multiply
def append_log(msg, log=[]):
log.append(msg)
return logCtrl + Enter runCtrl + Shift + Enter checkTab indentEsc then Tab to leave the editorCtrl + / comment