Development

Python Functions Explained: Arguments, Scope and Common Traps

A practical tour of how Python functions receive arguments, resolve variable names and hold on to state, with the mistakes that catch most learners.

SmartCampus Buddy TeamSeptember 15, 20267 min read

A function is a named, reusable block of code. In Python it is also an object, which means you can pass it around, store it in a list or return it from another function. Most bugs with functions come from three places: default arguments, variable scope and closures.

Parameters and arguments

Python supports positional arguments, keyword arguments, defaults, and two collectors: *args gathers extra positional arguments into a tuple, and **kwargs gathers extra keyword arguments into a dict.

def report(title, *scores, **options):
    print(title, scores, options)

report("Quiz", 8, 9, sep="-")   # Quiz (8, 9) {'sep': '-'}

The mutable default trap

Default values are evaluated once, when the function is defined, not on every call. A mutable default such as a list is therefore shared between calls.

def add(item, bucket=[]):
    bucket.append(item)
    return bucket

The safe pattern is to use None as the default and create the list inside the function:

def add(item, bucket=None):
    if bucket is None:
        bucket = []
    bucket.append(item)
    return bucket

Scope: where names are looked up

Python resolves a name by checking the local scope, then any enclosing function scopes, then the module (global) scope, then built-ins. Assigning to a name inside a function makes it local for the whole function, which is why count += 1 on a global raises UnboundLocalError. Prefer passing values in and returning results over reaching for global.

Closures and late binding

A closure remembers variables from the scope where it was created. It captures the variable, not its value at that moment, so closures created in a loop all see the final loop value unless you bind it, for example with a default argument (lambda x, i=i: x * i).

Key takeaways

  • Never use a mutable object as a default value; use None and create it inside.
  • Assignment inside a function creates a local name.
  • Closures capture variables, not values.
  • Give functions a single clear job and return values instead of relying on side effects.