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.

تحميل هذا الباب / Download this Chapterنسخة كاملة للدراسة بدون إنترنت، مع الأسئلة والإجابات والصور المتاحة.