04.2 - *args, **kwargs & Closures
*args — Variable Positional Arguments
*args collects any number of positional arguments into a tuple.
def add(*args):
return sum(args)
add(1, 2, 3) # 6
add(10, 20) # 30
add(1, 2, 3, 4, 5) # 15
# *args is just a convention — the * is what matters
def display(*values):
for v in values:
print(v)
display("a", "b", "c")
# a
# b
# c
**kwargs — Variable Keyword Arguments
**kwargs collects any number of keyword arguments into a dict.
def display_info(**kwargs):
for key, value in kwargs.items():
print(f" {key}: {value}")
display_info(name="Alice", age=30, city="Montreal")
# name: Alice
# age: 30
# city: Montreal
# Practical use: flexible config builder
def create_config(env, **options):
config = {"environment": env}
config.update(options)
return config
cfg = create_config("prod", debug=False, workers=4, max_connections=100)
Combining All Parameter Types
The order of parameters is strict:
def func(positional, /, regular, *args, keyword_only, **kwargs)
│ │ └── keyword-only (after *)
│ └── variadic positional
└── positional-only (Python 3.8+)
def mixed(a, b, *args, key1="default", key2=None, **kwargs):
print(f"a={a}, b={b}")
print(f"args={args}")
print(f"key1={key1}, key2={key2}")
print(f"kwargs={kwargs}")
mixed(1, 2, 3, 4, key1="hello", extra="value")
# a=1, b=2
# args=(3, 4)
# key1=hello, key2=None
# kwargs={'extra': 'value'}
Unpacking Arguments with * and **
Use * to unpack a list/tuple as positional arguments, ** to unpack a dict as keyword arguments:
def power(base, exponent):
return base ** exponent
args = (2, 10)
power(*args) # 2 ** 10 = 1024 (unpacks tuple)
kwargs = {"base": 3, "exponent": 4}
power(**kwargs) # 3 ** 4 = 81 (unpacks dict)
# Combine both
params = (2,)
options = {"exponent": 8}
power(*params, **options) # 256
# Useful for forwarding arguments
def wrapper(*args, **kwargs):
print("Before call")
result = some_function(*args, **kwargs)
print("After call")
return result
Positional-Only Parameters (Python 3.8+)
Parameters before / can only be passed positionally — not by name:
def divide(numerator, denominator, /):
return numerator / denominator
divide(10, 2) # OK: 5.0
divide(10, denominator=2) # TypeError!
# Use case: match built-in behavior
len("hello") # not len(obj="hello")
Keyword-Only Parameters
Parameters after * can only be passed by keyword:
def connect(host, port, *, ssl=True, timeout=30):
print(f"Connecting to {host}:{port} ssl={ssl}")
connect("db.com", 5432) # OK
connect("db.com", 5432, ssl=False) # OK
connect("db.com", 5432, False) # TypeError! ssl must be keyword
Closures — Functions that Remember
A closure is a function that captures variables from its enclosing scope even after that scope has finished.
def make_multiplier(factor):
"""Returns a function that multiplies by factor."""
def multiply(n):
return n * factor # factor is captured from enclosing scope
return multiply
double = make_multiplier(2)
triple = make_multiplier(3)
double(5) # 10
triple(5) # 15
double(8) # 16
Practical Closure Patterns
# Counter factory
def make_counter(start=0):
count = [start] # using list for mutability in Python 2 compat
def increment(step=1):
count[0] += step
return count[0]
return increment
counter = make_counter()
counter() # 1
counter() # 2
counter(5) # 7
# Memoization with closure
def make_cached(func):
cache = {}
def cached(*args):
if args not in cache:
cache[args] = func(*args)
return cache[args]
return cached
@make_cached
def fibonacci(n):
if n <= 1:
return n
return fibonacci(n-1) + fibonacci(n-2)
# Partial application
def make_adder(n):
return lambda x: x + n
add5 = make_adder(5)
add5(3) # 8
add5(10) # 15
functools.partial
functools.partial is the standard library way to create partial functions:
from functools import partial
def power(base, exponent):
return base ** exponent
square = partial(power, exponent=2)
cube = partial(power, exponent=3)
square(5) # 25
cube(3) # 27
# Useful with map/filter
from functools import partial
double = partial(lambda x, n: x * n, n=2)
list(map(double, [1, 2, 3, 4])) # [2, 4, 6, 8]
Key Vocabulary
| Term | Definition |
|---|---|
*args | Collects extra positional arguments into a tuple |
**kwargs | Collects extra keyword arguments into a dict |
| Unpacking | *list or **dict to pass iterable as arguments |
| Closure | Function that captures and remembers variables from its enclosing scope |
| Enclosing scope | The scope of the outer function in a nested function |
| Positional-only | Parameters before / that cannot be passed by name |
| Keyword-only | Parameters after * that must be passed by name |
functools.partial | Creates a new function with some arguments pre-filled |
Summary
*argscollects unlimited positional arguments as a tuple**kwargscollects unlimited keyword arguments as a dict- Argument order in definitions: positional → *args → keyword-only → **kwargs
- Use
*listand**dictto unpack when calling functions - Closures capture variables from their enclosing scope — useful for factory functions and state
functools.partialpre-fills arguments to create specialized versions of functions
📄️ 04.1 - Function Basics
Define reusable, documented functions with parameters, return values, default arguments, and type hints
📄️ 04.2 - *args & **kwargs
Write flexible functions with variable arguments, keyword arguments, and capture state with closures
📄️ 04.3 - Lambda & Functional
Write concise anonymous functions and apply functional programming patterns with Python's built-in tools
📄️ 04.4 - Decorators
Understand and write Python decorators to add cross-cutting concerns like logging, timing, caching, and validation
📄️ Lab - Module 04
Build a data processing pipeline using functions, lambdas, map/filter/reduce, closures, and decorators
📄️ Quiz - Module 04
30 questions on function basics, *args/**kwargs, lambda, map/filter/reduce, closures, and decorators