Advanced Functions & Parameters
Reviewed & published by Brayan K
Master every advanced Python function technique — from closures and decorators to metaprogramming, function factories, and production-level patterns used by FAANG engineers.
Part of the free Python course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
What You'll Learn in This Lesson
- • Positional vs keyword arguments and the mutable default trap
- • *args and **kwargs for flexible function signatures
- • First-class functions: passing, returning, and storing functions
- • Closures, lambda functions, and function factories
- • Keyword-only arguments and argument unpacking with * and **
🎯 What Makes Python Functions Special?
🏠 Real-World Analogy:
In Python, functions are like physical tools you can hold. You can put them in a toolbox (list), hand them to a friend (pass as argument), get them back (return value), or even create new tools on the fly!
| Capability | What It Means | Example |
|---|---|---|
| First-class objects | Functions are values like numbers | f = print |
| Assignable to variables | Store in a variable for later | greet = say_hello |
| Storable in collections | Keep functions in lists/dicts | ops = [add, sub] |
| Passable as arguments | Give to other functions | map(func, data) |
| Returnable as values | Functions can create functions | return inner_func |
| Capture outer scope | Remember variables (closures) | nonlocal count |
💡 Why This Matters: This gives Python functional power similar to JavaScript—enabling decorators, callbacks, and advanced patterns.
🧠 2. Positional vs Keyword Parameters (Deep Dive)
# Positional vs Keyword Arguments
# Positional arguments - order matters
def greet(name, age):
print(name, age)
greet("Alice", 25)
# Keyword arguments - order doesn't matter
greet(name="Alex", age=20)
greet(age=20, name="Alex")
# Keyword arguments improve:
# ✔ clarity
# ✔ safety
# ✔ order-independent calls
# ✔ API design
# ✅ Expected output:
# Alice 25
# Alex 20
# Alex 20- ✔ order-independent calls
- ✔ API design
⚙️ 3. Default Parameters (Correct & Incorrect Ways)
⚠️ The Mutable Default Trap (VERY Common Bug!)
One of Python's most notorious gotchas! Default values are created once when the function is defined, not each time it's called. This causes bugs with mutable defaults like lists or dicts.
| Default Type | Safe? | Why? |
|---|---|---|
| x=5 (int) | ✅ Yes | Immutable - can't be changed |
| x="hello" (str) | ✅ Yes | Immutable - can't be changed |
| x=[] (list) | ❌ DANGER | Mutable - shared across all calls! |
| ❌ DANGER | Mutable - shared across all calls! | |
| x=None | ✅ Yes | The correct fix for mutable defaults |
def create_user(role="guest"): # String is immutable - safe!
return {"role": role}
print(create_user()) # {'role': 'guest'}
print(create_user("admin")) # {'role': 'admin'}
# ✅ Expected output:
# {'role': 'guest'}
# {'role': 'admin'}❌ Dangerous default (THE BUG):
def add_item(item, items=[]): # ❌ List created ONCE when function defined!
items.append(item)
return items
print(add_item(1)) # [1] - looks fine...
print(add_item(2)) # [1, 2] - WAIT, where did 1 come from?!
print(add_item(3)) # [1, 2, 3] - They all share the SAME list!
# ✅ Expected output:
# [1]
# [1, 2]
# [1, 2, 3]✔ The Fix (use None pattern):
def add_item(item, items=None): # ✅ None is safe
if items is None:
items = [] # Create NEW list each time
items.append(item)
return items
print(add_item(1)) # [1]
print(add_item(2)) # [2] - Fresh list each time!
print(add_item(3)) # [3] - Each call is independent
# ✅ Expected output:
# [1]
# [2]
# [3]🔄 4. *args (Variadic Positional Parameters)
🤔 What is *args?
*args lets a function accept any number of positional arguments. They get collected into a tuple you can iterate over.
def total(*numbers): # *numbers catches ALL positional args
print(f"Received: {numbers}") # It's a tuple!
return sum(numbers)
print(total(1, 2, 3)) # Can pass 3 args
print(total(5, 10, 15, 20)) # Or 4 args
print(total(100)) # Or just 1!
# ✅ Expected output:
# Received: (1, 2, 3)
# 6
# Received: (5, 10, 15, 20)
# 50
# Received: (100,)
# 100🔧 5. **kwargs (Variadic Keyword Parameters)
🤔 What is **kwargs?
**kwargs lets a function accept any number of keyword arguments. They get collected into a dictionary.
def build_user(**info): # **info catches ALL keyword args
print(f"Received: {info}") # It's a dictionary!
return info
user = build_user(name="Alex", age=20, role="admin")
print(user) # {'name': 'Alex', 'age': 20, 'role': 'admin'}
# ✅ Expected output:
# Received: {'name': 'Alex', 'age': 20, 'role': 'admin'}
# {'name': 'Alex', 'age': 20, 'role': 'admin'}| Syntax | Collects | Stored As | Example Call |
|---|---|---|---|
| *args | Positional arguments | Tuple | f(1, 2, 3) |
| **kwargs | Keyword arguments | Dictionary | f(a=1, b=2) |
| *args, **kwargs | Both! | Tuple + Dict | f(1, 2, a=3) |
🧬 6. Argument Unpacking ( * and ** )
nums = [1, 2, 3]
print(*nums) # 1 2 3
def add(a, b, c):
return a + b + c
print(add(*nums))
# ✅ Expected output:
# 1 2 3
# 6data = {"x": 1, "y": 2}
def show(x, y):
print(x, y)
show(**data)
# ✅ Expected output:
# 1 2This is deeply used in:
- ✔ machine learning pipelines
- ✔ data transformations
- ✔ passing parameters through layers
- ✔ functional programming
🧩 7. First-Class Functions & Higher-Order Functions
A function passed into another function:
def apply(fn, value):
return fn(value)
print(apply(lambda n: n*n, 5)) # 25
def double(x):
return x * 2
print(apply(double, 10)) # 20
# ✅ Expected output:
# 25
# 20- ✔ AI model callbacks
- ✔ middleware
- ✔ map/filter/reduce
- ✔ backend hooks
- ✔ schedulers
🧠 8. Lambda Functions (Real Usage)
Beginners think lambdas are "inline shortcuts". Experts know lambdas power:
- ✔ functional pipelines
- ✔ small stateless transformations
users = [{"name": "Alex", "age": 20}, {"name": "Sam", "age": 30}]
sorted_users = sorted(users, key=lambda u: u["age"])
print(sorted_users)
# ✅ Expected output:
# [{'name': 'Alex', 'age': 20}, {'name': 'Sam', 'age': 30}]🔒 9. Closures (Python's Most Important Function Feature)
A closure is like a backpack that a function carries. When you create a function inside another function, the inner function "packs" any variables it needs from the outer function and keeps them forever—even after the outer function finishes!
| Term | Meaning |
|---|---|
| Closure | A function that "remembers" variables from where it was created |
| nonlocal | Keyword to modify (not just read) an outer variable |
| Free variable | A variable used in a function but defined outside it |
def counter():
count = 0 # This is the "backpack" variable
def inc():
nonlocal count # ← "I want to MODIFY count, not create a new one"
count += 1
return count
return inc # Return the inner function (with its backpack!)
# Create a counter - it remembers its own count
c = counter()
print(c()) # 1 - count is remembered!
print(c()) # 2 - still remembering!
print(c()) # 3 - it persists!
# Create another counter - it has its OWN backpack
c2 = counter()
print(c2()) # 1 - separate count!
# ✅ Expected output:
# 1
# 2
# 3
# 1- ✔ stateful utilities (counters, accumulators)
- ✔ caching (remember previous results)
- ✔ function factories (create customized functions)
- ✔ rate limiters (track call history)
- ✔ object-like behaviour without classes
🎁 10. Decorators (The Real Advanced-Level Skill)
Decorators transform functions and classes — used everywhere in Python frameworks.
def logger(fn):
def wrapper(*args, **kwargs):
print("Calling:", fn.__name__)
return fn(*args, **kwargs)
return wrapper
@logger
def greet():
print("Hello!")
greet()
# ✅ Expected output:
# Calling: greet
# Hello!- ✔ authentication
- ✔ rate limiting
- ✔ measuring execution time
- ✔ ORM mappings
- ✔ FastAPI / Flask route handling
This is one of Python's signature advanced features.
🧪 10b. Worked Example — Everything So Far in One Decorator
This is where the last few sections join up. A decorator is a closure (it remembers the function it was handed), it uses *args and **kwargs so it can wrap any function, and it has to hand the return value back or the decorated function silently starts returning None.
Read the comments before running it — each one says what the line does and what it costs you if you leave it out.
import functools
# A decorator is just a function that takes a function and returns a new one.
def log_calls(func):
# @functools.wraps copies the original name and docstring onto the wrapper.
# Leave it out and greet.__name__ comes back as "wrapper", which breaks
# help(), debuggers, and anything that inspects your functions.
@functools.wraps(func)
def wrapper(*args, **kwargs):
# *args collects positional arguments into a tuple.
# **kwargs collects keyword arguments into a dict.
# Together they let one wrapper accept ANY call signature.
shown = [repr(a) for a in args] + [f"{k}={v!r}" for k, v in kwargs.items()]
print(f"-> {func.__name__}({', '.join(shown)})")
result = func(*args, **kwargs) # unpack them straight back out again
print(f"<- {func.__name__} returned {result!r}")
return result # forget this line and greet() gives you None
return wrapper
@log_calls # exactly the same as writing: greet = log_calls(greet)
def greet(name, greeting="Hello"):
"""Return a greeting line."""
return f"{greeting}, {name}!"
print(greet("Ada")) # one positional argument
print(greet("Grace", greeting="Hi")) # positional + keyword
# functools.wraps kept the identity of the original function:
print(greet.__name__)
print(greet.__doc__)
# ✅ Expected output:
# -> greet('Ada')
# <- greet returned 'Hello, Ada!'
# Hello, Ada!
# -> greet('Grace', greeting='Hi')
# <- greet returned 'Hi, Grace!'
# Hi, Grace!
# greet
# Return a greeting line.Notice the order of the printed lines. The -> line happens before the real function runs and the <- line after it, which is why decorators are the natural home for timing, logging, authentication and caching: they own the moment before and the moment after every call.
🎯 10c. Your Turn — A Decorator That Counts Calls
Two blanks, both of them the ideas this lesson is built on: the keyword that lets an inner function modify an outer variable, and the syntax that forwards every argument through untouched. Run it and check against the expected output.
import functools
# 🎯 YOUR TURN — finish the decorator that counts how often a function is called
# Fill in the two blanks marked ___
def count_calls(func):
calls = 0 # the closure variable — the "backpack"
@functools.wraps(func)
def wrapper(*args, **kwargs):
# 👉 replace ___ with the keyword that lets you MODIFY calls
# (without it, calls += 1 raises UnboundLocalError)
___ calls
calls += 1
print(f"{func.__name__} has been called {calls} time(s)")
# 👉 replace ___ so the original function receives whatever it was given
return func(___)
return wrapper
@count_calls
def add(a, b):
return a + b
print(add(2, 3)) # two positional arguments
print(add(10, b=5)) # one positional, one keyword
# ✅ Expected output:
# add has been called 1 time(s)
# 5
# add has been called 2 time(s)
# 15🚀 11. Function Factories (Dynamic Function Creation)
def multiply_by(n):
def inner(x):
return n * x
return inner
double = multiply_by(2)
triple = multiply_by(3)
print(double(10)) # 20
print(triple(10)) # 30
# ✅ Expected output:
# 20
# 30- ✔ ML preprocessing
- ✔ generating optimised functions
- ✔ parameter-controlled utilities
- ✔ dynamic pipelines
🔁 12. Recursion (Pythonic Patterns)
def factorial(n):
return 1 if n <= 1 else n * factorial(n-1)
print(factorial(5)) # 120
# ✅ Expected output:
# 120- ✔ tree search
- ✔ JSON traversal
- ✔ directory crawling
- ✔ AI state exploration
- ✔ compilers and parsers
Tail recursion isn't optimised in Python—so you must use it strategically.
🧮 13. Memoization (Caching for High Performance)
🤔 What is Memoization?
Memoization means remembering the results of expensive function calls. If you call the function with the same inputs again, it returns the cached result instantly instead of recalculating.
| Without Memoization | With Memoization |
|---|---|
| fib(30) → 1+ million calculations | fib(30) → ~30 calculations |
| Exponential time O(2ⁿ) | Linear time O(n) |
| Takes seconds/minutes | Instant ⚡ |
from functools import lru_cache
@lru_cache(maxsize=None) # ← This one line adds caching!
def fib(n):
if n < 2:
return n
return fib(n-1) + fib(n-2)
# Without @lru_cache, fib(30) would take forever
# With it, it's instant!
print(fib(30)) # 832040 - calculated instantly!
print(fib(50)) # 12586269025 - still instant!
# ✅ Expected output:
# 832040
# 12586269025- ✔ AI & dynamic programming algorithms
- ✔ data pipelines with repeated queries
- ✔ expensive calculations (math, crypto)
- ✔ API caching (avoid repeated network calls)
🧱 14. Pure vs Impure Functions
def add(a, b):
return a + b
print(add(5, 3))
# ✅ Expected output:
# 8x = 0
def inc():
global x
x += 1
inc()
print(x)
# ✅ Expected output:
# 1- ✔ parallel execution
- ✔ reliability
🎛 15. Context Managers as Functionality
Context managers extend function behavior beyond decorators.
from contextlib import contextmanager
@contextmanager
def timer():
import time
start = time.time()
yield
print(f"Elapsed: {time.time() - start}s")
with timer():
sum(range(1000000))- ✔ file streams
- ✔ database sessions
- ✔ API connections
- ✔ locking mechanisms
- ✔ resource management
🕹 16. Putting It All Together — Advanced Example
A rate-limited API wrapper using:
- ✔ decorators
- ✔ *args / **kwargs
- ✔ higher-order functions
import time
def rate_limit(max_calls, interval):
calls = []
def decorator(fn):
def wrapper(*args, **kwargs):
nonlocal calls
now = time.time()
calls = [t for t in calls if now - t < interval]
if len(calls) >= max_calls:
raise Exception("Rate limit exceeded")
calls.append(now)
return fn(*args, **kwargs)
return wrapper
return decorator
@rate_limit(3, 5)
def fetch_data():
print("Fetching data...")
for _ in range(4):
try:
fetch_data()
except Exception as e:
print(e)This is real production-level engineering — used in APIs, SaaS, AI tools, and infra services.
🧩 17. Currying & Partial Function Application
🤔 What's the Difference?
| Technique | What It Does | Example |
|---|---|---|
| Currying | Transform f(a,b) into f(a)(b) | multiply(2)(3) |
| Partial | Pre-fill some arguments | double = partial(multiply, 2) |
def multiply(a): # First call: pass 'a'
def inner(b): # Second call: pass 'b'
return a * b # Use both!
return inner
# Create specialized functions
double = multiply(2) # "Lock in" 2 as first argument
triple = multiply(3) # "Lock in" 3 as first argument
print(double(10)) # 20 (2 * 10)
print(triple(10)) # 30 (3 * 10)
# ✅ Expected output:
# 20
# 30from functools import partial
def power(base, exponent):
return base ** exponent
# Pre-fill exponent=2 → creates a "square" function
square = partial(power, exponent=2)
# Pre-fill exponent=3 → creates a "cube" function
cube = partial(power, exponent=3)
print(square(5)) # 25 (5²)
print(cube(5)) # 125 (5³)
# ✅ Expected output:
# 25
# 125🧬 18. Function Introspection
def hello(name: str) -> str:
return f"Hello {name}"
print(hello.__name__)
print(hello.__annotations__)
print(hello.__code__.co_argcount)
# ✅ Expected output:
# hello
# {'name': <class 'str'>, 'return': <class 'str'>}
# 1- ✔ documentation generators
- ✔ API frameworks
- ✔ testing tools
🧱 19. Metaprogramming With Functions
def modify(func):
def wrapper(*a, **kw):
result = func(*a, **kw)
return f"[Modified] {result}"
return wrapper
@modify
def greet():
return "Hello"
print(greet()) # [Modified] Hello
# ✅ Expected output:
# [Modified] Hello- ✔ FastAPI route injections
- ✔ Django model generation
- ✔ SQLAlchemy ORM mappings
- ✔ automatic validations
- ✔ custom DSLs
🔁 20. Function Pipelines (Functional Composition)
def compose(f, g):
return lambda x: f(g(x))
def increment(x): return x + 1
def double(x): return x * 2
pipeline = compose(double, increment)
print(pipeline(5)) # (5+1)*2 = 12
# ✅ Expected output:
# 12Functional composition is used in:
- ✔ ETL pipelines
- ✔ data cleaning
- ✔ ML transformations
- ✔ text processing
- ✔ audio/image pipelines
📦 21. Callables Beyond Functions
class Multiplier:
def __init__(self, factor):
self.factor = factor
def __call__(self, value):
return value * self.factor
double = Multiplier(2)
print(double(10)) # 20
# ✅ Expected output:
# 20- ✔ function-like objects with state
- ✔ ML layers (PyTorch uses this!)
- ✔ command objects
- ✔ configurable utilities
🧲 22. Dynamic Dispatch (Single Dispatch)
from functools import singledispatch
@singledispatch
def display(x):
print("Default:", x)
@display.register(int)
def _(x):
print("Integer:", x)
@display.register(list)
def _(x):
print("List:", x)
display(10)
display([1,2,3])
# ✅ Expected output:
# Integer: 10
# List: [1, 2, 3]- ✔ cleaner APIs
- ✔ automatic type routing
- ✔ easy overloading without OOP
⚙️ 23. Decorators With Arguments (Decorator Factories)
def repeat(n):
def wrapper(func):
def inner(*a, **kw):
for _ in range(n):
func(*a, **kw)
return inner
return wrapper
@repeat(3)
def greet():
print("Hello")
greet()
# ✅ Expected output:
# Hello
# Hello
# HelloThis is essential for:
- ✔ authentication systems
- ✔ logging levels
- ✔ retry mechanisms
- ✔ dynamic configuration
🧪 24. Creating Custom Decorators for Error Handling
def safe(func):
def wrapper(*a, **kw):
try:
return func(*a, **kw)
except Exception as e:
print("Error:", e)
return wrapper
@safe
def risky(x):
return 10 / x
risky(0) # prints error instead of crashing
# ✅ Expected output:
# Error: division by zero- ✔ production pipelines
- ✔ monitoring systems
- ✔ API endpoints
- ✔ retry logic
🔍 25. Benchmarking Functions
import time
def slow():
sum(i*i for i in range(20000))
start = time.perf_counter()
slow()
end = time.perf_counter()
print("Time:", end - start)- ✔ optimisation
- ✔ heavy loops
- ✔ performance regressions
🚀 26. Real-World: Dynamic API Client Generator
def api_client(base_url):
def make_request(endpoint):
def call(**params):
print("GET", base_url + endpoint, params)
return call
return make_request
github = api_client("https://api.github.com")
get_user = github("/users")
get_user(username="torvalds")
# ✅ Expected output:
# GET https://api.github.com/users {'username': 'torvalds'}This is how real SDKs are created.
🧨 27. Using Functions to Create DSLs
def select(*fields):
return {"select": fields}
def where(**conditions):
return {"where": conditions}
query = {**select("name", "age"), **where(id=5)}
print(query)
# ✅ Expected output:
# {'select': ('name', 'age'), 'where': {'id': 5}}DSLs are used in:
- ✔ ORM queries
- ✔ configuration languages
- ✔ build tools
- ✔ infrastructure scripts
🧠 28. Advanced Factory Patterns With Functions
def serializer(fmt):
if fmt == "json":
import json
return json.dumps
if fmt == "repr":
return repr
to_json = serializer("json")
print(to_json({"key": "value"}))
# ✅ Expected output:
# {"key": "value"}🎛 29. Using Functions as Middleware Chains
def middleware_chain(func_list):
def call(value):
for f in func_list:
value = f(value)
return value
return call
pipeline = middleware_chain([
lambda x: x + 1,
lambda x: x * 5,
str
])
print(pipeline(10)) # "55"
# ✅ Expected output:
# 55- ✔ web frameworks
- ✔ logging systems
- ✔ request processing
🧨 30. Part 1 Summary
You can now work with:
- ✔ introspection
- ✔ function factories
- ✔ recursion & memoization
- ✔ pure/impure patterns
- ✔ advanced dispatch
- ✔ context managers
🧠 31. Descriptors — The Hidden Power Behind Properties
class Descriptor:
def __get__(self, instance, owner):
return "Got value"
def __set__(self, instance, value):
print("Set:", value)
class Demo:
x = Descriptor()
d = Demo()
print(d.x)
d.x = 10
# ✅ Expected output:
# Got value
# Set: 10- ✔ dataclasses
- ✔ ORM fields (Django, SQLAlchemy)
- ✔ class-level validators
- ✔ computed attributes
🔧 32. Metaclasses + Functions = Dynamic Class Construction
class Meta(type):
def __new__(cls, name, bases, attrs):
attrs['created_by'] = 'Meta'
return super().__new__(cls, name, bases, attrs)
class User(metaclass=Meta):
pass
print(User.created_by)
# ✅ Expected output:
# Meta- ✔ Django ORM
- ✔ SQLAlchemy
- ✔ DRF serializers
- ✔ TensorFlow layers
🎛 33. Using Functions to Build Plugins
PLUGINS = {}
def plugin(name):
def register(func):
PLUGINS[name] = func
return func
return register
@plugin("compress")
def compress(data):
return data[:10]
print(PLUGINS)Plugins are used in:
- ✔ VS Code extensions
- ✔ Blender addons
- ✔ Flask extensions
- ✔ AI model hooks
- ✔ Code formatters
🧩 34. Higher-Order Error Handling Patterns
def rescue(default=None):
def wrapper(func):
def inner(*a, **kw):
try: return func(*a, **kw)
except: return default
return inner
return wrapper
@rescue(default="failed")
def risky(x):
return 10/x
print(risky(0)) # "failed"
# ✅ Expected output:
# failed- ✔ retry decorators
- ✔ exponential backoff
- ✔ circuit breakers
- ✔ graceful degradation
🧬 35. Partial Evaluation & Runtime Specialisation
from functools import partial
def logistic(x, L, k, x0):
return L / (1 + 2.71828 ** (-k*(x-x0)))
fast_logistic = partial(logistic, L=1, k=0.5)
print(fast_logistic(10, x0=5))
# ✅ Expected output:
# 0.9241417020900337Partial evaluation is used in:
- ✔ machine learning
- ✔ neural network training loops
- ✔ optimisation algorithms
- ✔ scientific computing
- ✔ compiler design
⚡ 36. Memoization Variants (Custom TTL Cache)
import time
def ttl_cache(seconds):
def wrapper(func):
store = {}
def inner(*a):
now = time.time()
if a in store:
value, t = store[a]
if now - t < seconds:
return value
result = func(*a)
store[a] = (result, now)
return result
return inner
return wrapper
@ttl_cache(5)
def expensive():
return "computed"This is the basis of:
- ✔ caching middleware
- ✔ API clients
- ✔ ML model caching
- ✔ search engines
🧱 37. Turning Functions Into Stateful Machines
def counter():
n = 0
def inner():
nonlocal n
n += 1
return n
return inner
c = counter()
print(c()) # 1
print(c()) # 2
# ✅ Expected output:
# 1
# 2Stateful function techniques create:
- ✔ batching systems
- ✔ throttlers
- ✔ generators
- ✔ stream processors
- ✔ real-time systems
🧠 38. Function Composition for Data Pipelines
def compose(*functions):
def pipeline(value):
for f in functions:
value = f(value)
return value
return pipeline
clean = compose(
lambda x: x.strip(),
lambda x: x.lower(),
lambda x: x.replace("!", "")
)
print(clean(" HELLO! "))
# ✅ Expected output:
# hello- ✔ Pandas transformations
- ✔ NLP preprocessing
- ✔ audio pipelines
- ✔ AI data augmentation
🧲 39. Callback Systems (Events, Hooks & Signals)
callbacks = []
def on_event(func):
callbacks.append(func)
return func
@on_event
def notify():
print("Notified!")
for f in callbacks:
f()
# ✅ Expected output:
# Notified!Callbacks are the foundation of:
- ✔ GUI systems
- ✔ Async programming
- ✔ Game engines
- ✔ ML training loops
🧨 40. Advanced Use of yield for Coroutines
def processor():
total = 0
while True:
x = yield total
total += x
p = processor()
next(p)
print(p.send(5)) # 5
print(p.send(3)) # 8- ✔ continuous data processing
- ✔ actor-like systems
- ✔ streaming architectures
- ✔ log processors
- ✔ incremental ML pipelines
🧮 41. Decorators That Modify Function Signatures
from functools import wraps
import inspect
def rename_args(**new_names):
def wrapper(func):
sig = inspect.signature(func)
print("Original sig:", sig)
return func
return wrapper
@rename_args(x="value")
def test(x):
pass
# ✅ Expected output:
# Original sig: (x)This kind of technique builds:
- ✔ click (CLI library)
- ✔ Flask request routing
🎛 42. Building an Advanced Pipeline Framework
class Pipeline:
def __init__(self):
self.steps = []
def step(self, func):
self.steps.append(func)
return func
def run(self, input):
for f in self.steps:
input = f(input)
return input
p = Pipeline()
@p.step
def clean(x): return x.strip()
@p.step
def lower(x): return x.lower()
print(p.run(" HELLO "))
# ✅ Expected output:
# helloThis mirrors real frameworks like Airflow or spaCy.
🎉 Final Wrap-Up
You now understand every advanced Python function mechanism:
- ✔ first-class functions
- ✔ lambda pipelines
- ✔ functional composition
- ✔ metaprogramming
- ✔ plugin architecture
- ✔ partial evaluation
- ✔ advanced caching
- ✔ pipeline engines
- ✔ descriptors
- ✔ metaclasses
- ✔ dynamic function creation
- ✔ stateful closures
- ✔ coroutine interaction
- ✔ generator processors
You've reached a level of Python function mastery that only senior engineers, framework authors, or ML system architects typically achieve.
🎯 Mini-Challenge: Build a Decorator Factory
A decorator factory is a decorator you can configure — @repeat(times=3) rather than plain @repeat. It is three nested functions deep, which is exactly why it is worth writing once by hand. Only the outline is given below; no logic.
# 🎯 MINI-CHALLENGE: a decorator factory — @repeat(times=3)
#
# Three layers, from the outside in:
# 1. def repeat(times): the FACTORY — takes the setting, returns a decorator
# 2. def decorator(func): the decorator — takes the function, returns a wrapper
# 3. def wrapper(*args, **kwargs):
# 4. results = []
# 5. call func(*args, **kwargs) "times" times, appending each result
# 6. return results
# 7. return wrapper
# 8. return decorator
#
# Then use it:
# @repeat(times=3)
# def roll():
# return 4
#
# print(roll())
#
# ✅ Expected output:
# [4, 4, 4]
# your code hereIf you get TypeError: decorator() missing 1 required positional argument, the usual cause is returning decorator(func) instead of decorator — the factory must hand back the function itself, not the result of calling it.
📋 Quick Reference — Advanced Functions
| Syntax | What it does |
|---|---|
| *args | Collect extra positional arguments |
| **kwargs | Collect extra keyword arguments |
| functools.partial(fn, x) | Fix one or more arguments |
| inspect.signature(fn) | Introspect function parameters |
| lambda x: x * 2 | Create an inline anonymous function |
🏆 Lesson Complete!
You've mastered every advanced function technique Python has — from variadic arguments to partial application and function introspection.
Practice quiz
What is the bug in 'def add_item(item, items=[])'?
- Lists cannot be parameters
- items must come first
- The default list is created once and shared across every call, so items leak between calls
- It is too slow
Answer: The default list is created once and shared across every call, so items leak between calls. Default values are created once at definition time, so a mutable default like [] is reused across calls and accumulates data.
What is the correct fix for a mutable default argument?
- Default to None, then do 'if items is None: items = []' inside
- Use items=() instead
- Make items required
- Use a global list
Answer: Default to None, then do 'if items is None: items = []' inside. The None sentinel pattern gives each call a fresh list, avoiding the shared-default trap.
Inside a function, what does *args collect arguments into?
- A list
- A dictionary
- A set
- A tuple
Answer: A tuple. *args gathers extra positional arguments into a tuple you can iterate over.
Inside a function, what does **kwargs collect arguments into?
- A tuple
- A dictionary
- A list
- A namedtuple
Answer: A dictionary. **kwargs gathers extra keyword arguments into a dictionary mapping names to values.
What keyword lets an inner function MODIFY a variable from its enclosing function?
- nonlocal
- global
- static
- extern
Answer: nonlocal. nonlocal lets a closure modify (not just read) a variable in the enclosing function's scope; without it Python creates a new local.
What does @lru_cache add to a function like a recursive fib?
- Logging
- Type checking
- Memoization — cached results so repeated inputs return instantly
- Parallel execution
Answer: Memoization — cached results so repeated inputs return instantly. @lru_cache from functools caches results, turning exponential recursive fib into linear time by reusing computed values.
With 'square = partial(power, exponent=2)', what does square(5) return for power(base, exponent)=base**exponent?
- 10
- 25
- 32
- 7
Answer: 25. partial pre-fills exponent=2, so square(5) computes 5**2 = 25.
What does 'compose(double, increment)' (f(g(x))) return for input 5, where increment adds 1 and double multiplies by 2?
- 11
- 10
- 7
- 12
Answer: 12. compose(double, increment)(5) = double(increment(5)) = double(6) = 12.
What makes a function 'pure'?
- It uses global variables
- Same input always gives the same output with no side effects
- It prints its result
- It modifies its arguments
Answer: Same input always gives the same output with no side effects. A pure function has no side effects and always returns the same output for the same input — easier to test and parallelize.
How is a callable class created?
- By defining __init__ only
- By inheriting from function
- By defining a __call__ method so instances can be invoked like functions
- By using @callable
Answer: By defining a __call__ method so instances can be invoked like functions. Defining __call__ makes instances callable, e.g. double = Multiplier(2); double(10) — giving function-like objects with state.
Continue this course
- Previous: Decorators & Advanced Features
- Next: Higher-Order Functions & Function Factories — Pass and return functions, build factories and pipelines
- Quick reference: Python cheat sheet