Functions, Scope & Modules
Why Functions Are the Unit of Thought
A function is not just a way to avoid repeating code. It is a named, callable unit of computation that takes inputs, does work, and returns outputs. Good Python code is almost entirely functions and classes working together — understanding them deeply is the single biggest lever you have for writing better code.
Python functions are first-class objects. They can be stored in variables, passed as arguments, returned from other functions, and stored in data structures. This is not a curiosity — it enables entire programming paradigms: callbacks, decorators, functional pipelines.
Defining Functions
The def keyword creates a function object and binds it to a name. The body runs only when called.
A function without an explicit return statement returns None. Always be intentional about return values.
Docstrings
Every non-trivial function should have a docstring. Python treats the first string literal in a function body as its documentation. Tools like help(), IDEs, and documentation generators read it automatically.
Parameters: The Full Picture
Python has five distinct parameter types. Most Python developers only use three, but knowing all five prevents confusion when you encounter them.
Positional and Keyword Arguments
Any parameter can be passed positionally (by order) or by keyword (by name). Keyword arguments can appear in any order.
Default Parameter Values
Parameters with defaults are optional at the call site. Always put defaulted parameters after non-defaulted ones.
Critical rule: Never use a mutable object (list, dict) as a default value. Python creates the default object once when the function is defined, not each time it is called.
*args and **kwargs
*args collects extra positional arguments into a tuple. **kwargs collects extra keyword arguments into a dict. They let you write functions that accept a variable number of arguments.
Keyword-Only and Positional-Only Parameters
Parameters after a bare * are keyword-only. Parameters before a / are positional-only. These are useful for designing clean APIs.
Scope: The LEGB Rule
When Python encounters a name, it searches four scopes in order: Local, Enclosing, Global, Built-in. This is the LEGB rule. Understanding it eliminates an entire class of bugs.
- Local — names defined inside the current function
- Enclosing — names in the enclosing function (for nested functions)
- Global — names defined at module level
- Built-in — Python's built-in names (
len,print,range, etc.)
global and nonlocal
Use global to rebind a module-level name from inside a function. Use nonlocal to rebind an enclosing function's name. Both are code smells in large programs — prefer returning values or using classes — but they have legitimate uses.
Closures: Functions That Remember
A closure is a function that captures variables from its enclosing scope even after that scope has finished executing. The function object carries a reference to those variables.
This is how Python implements things like decorators, factory functions, and stateful callbacks without needing a class.
First-Class Functions
Because functions are objects, you can do anything with them that you can do with any other value.
Lambda Expressions
A lambda is an anonymous function limited to a single expression. It is syntactic sugar — everything a lambda can do, a regular def can do better. Use lambdas only for short throwaway functions passed to sorted, map, filter, or similar.
When NOT to use lambda: if the body is complex, if you need a docstring, if you need to name it and reuse it, or if it would be clearer as a def.
Modules: Organizing Code
A module is simply a Python file. When you write import math, Python finds math.py in its search path (sys.path), executes it, and binds the resulting module object to the name math in your current namespace.
The __name__ == "__main__" Guard
When Python runs a file directly, it sets __name__ to "__main__". When a file is imported as a module, __name__ is set to the module's name. The guard lets you write code that only runs when the file is executed directly, not when imported.
The Standard Library Highlights
Python's standard library is enormous. Here are the modules you will use constantly:
PROJECT: Text Statistics Analyzer
A real utility that computes several statistics about a piece of text — demonstrating functions, collections.Counter, string methods, and module-level organization.
PROJECT: Utility Module Structure
In a real project, you would organize reusable functions into a module. Here is what a utility module looks like — the structure and import patterns:
Challenge
Test your understanding with these exercises:
-
Memoization from scratch — write a
memoize(func)function that wraps any function and caches its results in a dict. Callmemoize(fibonacci)(35)and compare the speed to the naive recursive version. -
Partial application — implement your own
partial(func, *partial_args)function (then compare tofunctools.partial). Use it to createdoubleandtriplefrom amultiply(a, b)function. -
Scope puzzle — predict what this prints before running it:
- Text analyzer extension — add a
flesch_reading_easescore to the Text Statistics Analyzer. The formula is:
Estimate syllable count by counting vowel groups per word.
- Module design — design a
validatorsmodule with functions:is_email,is_url,is_phone_number,is_strong_password. Each returnsTrue/False. Add avalidate(value, *validator_fns)function that runs all validators and returns a list of failures.
Key Takeaways
- Functions are objects — storing, passing, and returning them unlocks powerful patterns
- The LEGB rule governs every name lookup; understanding it eliminates mysterious bugs
- Never use mutable default arguments — use
Noneand create inside the function *argsand**kwargsmake APIs flexible; keyword-only parameters make them safe- Closures capture variables from enclosing scope — they are the foundation of decorators and factory functions
- Lambda is for short, throwaway key functions — reach for
defotherwise - The
__name__ == "__main__"guard separates executable scripts from importable modules collections.Counter,collections.defaultdict, anditertoolssolve 90% of common iteration problems elegantly