Goal
You’ll understand variable scope well enough to predict where a variable is accessible, and be able to use default arguments correctly, including a genuinely dangerous pitfall involving mutable defaults.
Learn
A variable’s scope determines where it can be accessed. A variable created inside a function is local — it only exists inside that function, and disappears once the function finishes running:
def my_function():
x = 5 # local to my_function
print(x)
my_function()
print(x) # NameError! x doesn't exist out here Functions can accept default arguments — a fallback value used if the caller doesn’t provide one:
def greet(name, greeting="Hello"):
return f"{greeting}, {name}!"
greet("Ahmed") # "Hello, Ahmed!"
greet("Ahmed", "Welcome") # "Welcome, Ahmed!" Here’s a genuinely dangerous, well-known Python pitfall: using a mutable default argument, like a list, can cause deeply confusing bugs, because the default value is created only once, when the function is defined — not fresh each time it’s called:
def add_item(item, my_list=[]): # DANGEROUS
my_list.append(item)
return my_list
add_item("a") # ["a"]
add_item("b") # ["a", "b"] — NOT a fresh empty list! The safe pattern is using None as the default, then creating a fresh list inside the function body if needed:
def add_item(item, my_list=None):
if my_list is None:
my_list = []
my_list.append(item)
return my_list Decision Task
You write def append_to_list(item, target=[]): and call it three separate times with different single items, expecting three independent single-item lists back. Before reading on: what actually happens, and why?
Show Answer
All three calls actually share and accumulate into the exact same list, since the default empty list is created only once, at function definition time, not freshly on each call. Instead of three independent single-item lists, you’d get an ever-growing list across calls — the first call returns one item, the second returns two accumulated items, and so on, which is almost never the intended behavior.
Common Mistake
Using a mutable object like a list or dictionary as a default argument value. Since the default is created exactly once at function definition time, not fresh on every call, this creates a shared, accumulating object across every call that doesn’t explicitly provide its own value — a genuinely well-known Python trap, not an obscure edge case.
Practice Questions
1. Write a function called power that takes a base and an exponent, with exponent defaulting to 2 (so power(5) returns 25).
Show Answer
def power(base, exponent=2):\n return base ** exponent
2. Why does a variable created inside a function disappear once the function finishes running?
Show Answer
Because it has local scope — it only exists within that function’s execution, and is cleaned up once the function returns.
3. What’s the safe pattern for a function that needs a fresh, empty list as a default argument each call?
Show Answer
Use None as the actual default, then check “if my_list is None: my_list = []” inside the function body, creating a genuinely fresh list on each call.
4. True or False: mutable default arguments like empty lists are recreated fresh every time the function is called.
Show Answer
False — this is the exact dangerous misconception this lesson addresses; they’re created once, at function definition time, and shared/accumulated across calls that don’t provide their own value.
5. Given the safe pattern shown in this lesson, why does checking “if my_list is None” work correctly, while a mutable default wouldn’t?
Show Answer
None is immutable and comparing against it doesn’t accumulate state; a fresh list is only created inside the function body each time it’s actually needed, rather than being pre-created once and shared across every call.
Try It Yourself
Without looking back, write a function called log_message that takes a message and an optional list to append to, using the safe None-default pattern from this lesson, correctly avoiding the mutable default trap.
Show Answer
def log_message(message, log=None):\n if log is None:\n log = []\n log.append(message)\n return log — this correctly creates a fresh list each call unless the caller explicitly provides their own.
Quick Check
1. What is a local variable’s scope?
Show Answer
It only exists and is accessible within the function it was created in.
2. What does a default argument provide?
Show Answer
A fallback value used automatically if the caller doesn’t provide one explicitly.
3. Why are mutable default arguments (like empty lists) genuinely dangerous in Python?
Show Answer
They’re created only once at function definition time, not fresh on each call, causing shared, accumulating state across calls that don’t provide their own value.
4. What’s the safe alternative pattern for a mutable default value?
Show Answer
Default to None, then create a fresh mutable object inside the function body if the argument wasn’t provided.
5. Can you access a function’s local variable from outside that function?
Show Answer
No — attempting to do so raises a NameError, since local variables don’t exist outside their defining function.