Functions, Arguments, Scope, and Closures

Functions are how you package logic so you can reuse and test it. Algorithm code leans heavily on functions, including recursive ones. This page covers how arguments work, how Python resolves names, and the two traps that catch beginners: mutable default arguments and scope confusion. Read Python for DSA first if you have not.

Positional and keyword arguments#

Arguments can be passed by position or by name. Named (keyword) arguments make calls readable and order-independent.

1def make_range(start, stop, step):
2    return list(range(start, stop, step))
3
4print(make_range(0, 10, 2))                    # positional
5print(make_range(stop=10, start=0, step=2))    # keyword, any order

You can give parameters defaults. Parameters with defaults must come after those without.

1def greet(name, greeting="Hello"):
2    return f"{greeting}, {name}"
3
4print(greet("Ada"))                 # Hello, Ada
5print(greet("Ada", "Welcome"))      # Welcome, Ada
6print(greet("Ada", greeting="Hi"))  # Hi, Ada

*args and **kwargs#

*args collects extra positional arguments into a tuple. **kwargs collects extra keyword arguments into a dict. They let a function accept a variable number of inputs.

 1def total(*args):
 2    result = 0
 3    for n in args:
 4        result += n
 5    return result
 6
 7print(total(1, 2, 3))          # 6
 8print(total(10, 20))           # 30
 9
10def describe(**kwargs):
11    for key, value in kwargs.items():
12        print(f"{key} = {value}")
13
14describe(name="Ada", role="pioneer")

The same star syntax unpacks a collection back into arguments when calling.

1def add(a, b, c):
2    return a + b + c
3
4nums = [1, 2, 3]
5print(add(*nums))              # 6, unpacks the list
6
7opts = {"a": 4, "b": 5, "c": 6}
8print(add(**opts))             # 15, unpacks the dict

The mutable default argument trap#

This is one of Python’s most famous gotchas. Default argument values are created once, when the function is defined, not each time it is called. A mutable default like a list or dict is shared across every call.

1# BROKEN: the list is created once and reused
2def append_broken(item, bucket=[]):
3    bucket.append(item)
4    return bucket
5
6print(append_broken(1))   # [1]
7print(append_broken(2))   # [1, 2]  surprise, the same list
8print(append_broken(3))   # [1, 2, 3]

The fix is to default to None and build a fresh object inside the function.

1def append_fixed(item, bucket=None):
2    if bucket is None:
3        bucket = []
4    bucket.append(item)
5    return bucket
6
7print(append_fixed(1))    # [1]
8print(append_fixed(2))    # [2], fresh list each time
9print(append_fixed(3))    # [3]

Always use None as the default for lists, dicts, and sets. Immutable defaults (numbers, strings, tuples, None) are safe because they cannot be changed in place.

LEGB: how Python finds a name#

When you use a name, Python searches four scopes in order: Local, Enclosing, Global, Built-in. The first match wins.

 1x = "global"
 2
 3def outer():
 4    x = "enclosing"
 5    def inner():
 6        x = "local"
 7        print(x)      # local, found in Local first
 8    inner()
 9    print(x)          # enclosing
10
11outer()
12print(x)              # global
13print(len)            # built-in name found last

By default, assigning to a name inside a function creates a new local variable. To rebind an outer name, use global (for module level) or nonlocal (for an enclosing function).

 1counter = 0
 2
 3def bump():
 4    global counter
 5    counter += 1
 6
 7bump()
 8bump()
 9print(counter)        # 2
10
11def make_stepper():
12    total = 0
13    def step():
14        nonlocal total
15        total += 1
16        return total
17    return step
18
19s = make_stepper()
20print(s(), s(), s())  # 1 2 3

Closures: functions that remember#

A closure is a function that captures variables from its enclosing scope and keeps them alive after that scope returns. make_stepper above is a closure. Here is a classic factory pattern.

1def multiplier(factor):
2    def multiply(n):
3        return n * factor      # remembers factor
4    return multiply
5
6double = multiplier(2)
7triple = multiplier(3)
8print(double(5))               # 10
9print(triple(5))               # 15

Each returned function carries its own captured factor. Closures power decorators, memoization caches, and callbacks. They also explain why the LEGB rules matter: the inner function reaches into the Enclosing scope to find factor.

With functions, scope, and closures in hand, you are ready for recursion and the rest of the 60-day curriculum. Watch how many times recursive functions call themselves and check the Big-O cheat sheet as you work through the algorithm lessons.