Functions, Parameters, Scope, and Modular Code Organization
Functions are the unit of reuse: named blocks that take inputs, do one job, and hand back a result. This domain tests parameter passing, return behaviour, name resolution, and how code is split across modules so that changes stay local.
Defining and calling
A definition names the function and its parameters; a call supplies arguments. Positional arguments bind by order, keyword arguments bind by name. Default values make an argument optional. A function with no return returns None (or void), and print writes to the console without producing a value - a frequent source of None appearing where a number was expected.
Scope
Names resolve inside-out: local, enclosing, global, built-in (LEGB in Python). Assigning to a name inside a function creates a local unless the language says otherwise, so modifying a global from inside a function usually needs an explicit declaration or, better, a parameter and a return value. Arguments are passed by object reference: rebinding the parameter inside the function does not affect the caller, but mutating the object does.
Worked example
def add_item(item, bucket=[]):
bucket.append(item)
return bucket
Default values are evaluated once, when the function is defined, not on each call. The same list is therefore reused: add_item('a') returns ['a'], and add_item('b') returns ['a', 'b']. The fix is bucket=None followed by if bucket is None: bucket = [].
Modular organisation
Group related functions in a module, expose a small public surface, and import rather than copy. Keep functions pure where possible: same inputs, same output, no hidden state. That makes them testable in isolation.
Traps
- Forgetting
return, then using the result. - Shadowing a built-in name such as
listorsum. - Mutable default arguments shared across calls.
- Changing a global variable instead of returning a value.
- A recursive function without a reachable base case.