Welcome to one of the most powerful lessons in your Python journey.
In earlier lessons you learned how to write functions · blocks of reusable code that take inputs and return outputs. In this lesson you will meet four advanced functional programming tools that Python developers use every single day in real-world projects:
| Tool | What it does |
|---|---|
| Decorators | Wrap a function to add extra behaviour without changing its code |
| Lambda Functions | Write tiny, one-line anonymous functions on the spot |
| Recursion | Write functions that solve problems by calling themselves |
| Generators | Produce values one at a time to save memory |
Each concept is a genuine superpower. By the end of this lesson you will understand them deeply, practise them with guided exercises, and combine them in a realistic mini-project.
💡 Teacher's Note: These topics are interconnected. Lambdas are often used inside decorators. Generators often use recursion. Take your time with each section before moving on.
Before diving in, make sure you are comfortable with the following. Each one is briefly reviewed below.
Functions
A function is a named block of reusable code.
def greet(name):
return "Hello, " + name
print(greet("Alice"))Output:
Hello, AliceFunctions Are Objects in Python
This is the most important prerequisite for this lesson.
In Python, a function is just an object · exactly like a number, a string, or a list. That means you can:
- Store a function inside a variable
- Pass a function as an argument to another function
- Return a function from a function
def say_hello():
print("Hello!")
# Store the function in a variable (no parentheses = no call)
my_func = say_hello
# Call it through the variable
my_func()Output:
Hello!🔑 Key insight:
say_hello(no parentheses) is the function object.say_hello()(with parentheses) calls it and runs it. This distinction is critical for decorators.
Inner Functions
Python allows you to define a function inside another function.
def outer():
def inner():
print("I am the inner function")
inner() # call the inner function
outer()Output:
I am the inner functionWhat Is a Decorator?
Imagine you have a function that does something useful. Now you want to add extra behaviour to it · like logging when it runs, checking if the user is logged in, or measuring how long it takes · without changing the function's original code.
A decorator is a function that wraps around another function and adds that extra behaviour automatically.
Real-world analogy: Think of a coffee cup (your original function) and a cup sleeve (the decorator). The sleeve adds insulation and a holder without changing the coffee inside. You can put the same sleeve on any cup.
How a Decorator Works · Step by Step
Let's build one from scratch before using the shortcut syntax.
Step 1: A simple function we want to decorate
def say_hello():
print("Hello!")Step 2: Write a decorator function
def my_decorator(func): # receives the function as an argument
def wrapper(): # defines a new wrapping function
print("Before the function runs")
func() # calls the original function
print("After the function runs")
return wrapper # returns the wrapper (not calling it!)Step 3: Apply the decorator manually
say_hello = my_decorator(say_hello) # wrap say_hello
say_hello() # call the wrapped versionOutput:
Before the function runs
Hello!
After the function runsLet's trace exactly what happened:
my_decorator(say_hello)· passessay_helloasfuncinto the decorator- Inside the decorator,
wrapperis defined (but not called yet) wrapperis returned · it is now the newsay_hello- When we call
say_hello(), we are actually callingwrapper() wrapperprints "Before…", calls the originalfunc()which prints "Hello!", then prints "After…"
The @ Shortcut Syntax
Python provides a cleaner way to apply a decorator using the @ symbol. This is called decorator syntax or pie syntax.
def my_decorator(func):
def wrapper():
print("Before the function runs")
func()
print("After the function runs")
return wrapper
@my_decorator # This is EXACTLY the same as: say_hello = my_decorator(say_hello)
def say_hello():
print("Hello!")
say_hello()Output:
Before the function runs
Hello!
After the function runs💡
@my_decoratorplaced just above a function definition is a clean shorthand for wrapping it.
Decorating Functions That Have Arguments
What if the function you want to decorate takes arguments? You need to pass those arguments through the wrapper.
def my_decorator(func):
def wrapper(*args, **kwargs): # accepts any arguments
print("Something before")
result = func(*args, **kwargs) # passes them to the original function
print("Something after")
return result # returns whatever the original function returned
return wrapper
@my_decorator
def add(a, b):
return a + b
total = add(3, 4)
print("Result:", total)Output:
Something before
Something after
Result: 7What are args and kwargs?*
*args· collects any number of positional arguments into a tuplekwargs· collects any number of keyword** arguments into a dictionary- Together they allow your wrapper to work with any function, regardless of its parameter list
A Practical Decorator: Timing a Function
Here is a decorator you would actually use in real code · measuring how long a function takes to run:
import time
def timer_decorator(func):
def wrapper(*args, **kwargs):
start = time.time() # record start time
result = func(*args, **kwargs)
end = time.time() # record end time
print(f"{func.__name__} took {end - start:.4f} seconds")
return result
return wrapper
@timer_decorator
def slow_task():
time.sleep(1) # simulate a slow operation
print("Task done!")
slow_task()Output (approximate):
Task done!
slow_task took 1.0012 seconds🌍 Real-world use: Web frameworks like Flask and Django use decorators extensively for routing (
@app.route("/home")), authentication (@login_required), and caching.
A Practical Decorator: Access Control
is_logged_in = True # simulate login state
def require_login(func):
def wrapper(*args, **kwargs):
if is_logged_in:
return func(*args, **kwargs)
else:
print("Access denied. Please log in.")
return wrapper
@require_login
def view_dashboard():
print("Welcome to your dashboard!")
view_dashboard()Output (when logged in):
Welcome to your dashboard!Applying Multiple Decorators
You can stack decorators. They apply from bottom to top (closest to the function first).
def bold(func):
def wrapper():
return "<b>" + func() + "</b>"
return wrapper
def italic(func):
def wrapper():
return "<i>" + func() + "</i>"
return wrapper
@bold # applied second (outer)
@italic # applied first (inner)
def greet():
return "Hello"
print(greet())Output:
<b><i>Hello</i></b>🤔 Thinking prompt: What happens if you swap
@boldand@italic? Try it · what output do you get?
Common Beginner Mistakes with Decorators
Mistake 1: Calling the function instead of passing it
# ❌ WRONG
decorated = my_decorator(say_hello()) # say_hello() calls it and passes None
# ✅ CORRECT
decorated = my_decorator(say_hello) # pass the function objectMistake 2: Forgetting to return the wrapper
# ❌ WRONG — decorator returns None
def my_decorator(func):
def wrapper():
func()
# forgot: return wrapper
# ✅ CORRECT
def my_decorator(func):
def wrapper():
func()
return wrapperMistake 3: Forgetting to call the original function inside wrapper
# ❌ WRONG — original function never runs
def my_decorator(func):
def wrapper():
print("Before")
# forgot: func()
return wrapperWhat Is a Lambda Function?
A lambda function is a small, anonymous (nameless) function written in a single line.
- Anonymous means it has no
defkeyword and no name - It is used when you need a short, throwaway function
- It can only contain a single expression (not multiple lines of code)
Syntax:
lambda arguments : expressionYour First Lambda
# Regular function
def double(x):
return x * 2
# Equivalent lambda
double_lambda = lambda x: x * 2
print(double(5)) # Output: 10
print(double_lambda(5)) # Output: 10Output:
10
10Both do exactly the same thing. The lambda is just shorter.
Lambda With Multiple Arguments
add = lambda a, b: a + b
print(add(3, 7))Output:
10full_name = lambda first, last: first + " " + last
print(full_name("Ada", "Lovelace"))Output:
Ada LovelaceWhy Use Lambda? · The Power Comes With Other Functions
The real power of lambda appears when you use it inside other functions that accept a function as an argument. The most common examples are map(), filter(), and sorted().
Lambda with map()
map(function, list) applies a function to every item in a list.
numbers = [1, 2, 3, 4, 5]
doubled = list(map(lambda x: x * 2, numbers))
print(doubled)Output:
[2, 4, 6, 8, 10]Without lambda, you'd need to write a separate function:
def double(x):
return x * 2
doubled = list(map(double, numbers))Lambda makes this a one-liner.
Lambda with filter()
filter(function, list) keeps only the items where the function returns True.
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
evens = list(filter(lambda x: x % 2 == 0, numbers))
print(evens)Output:
[2, 4, 6, 8, 10]Lambda with sorted()
sorted(list, key=function) sorts a list using a custom key.
students = [
{"name": "Alice", "grade": 85},
{"name": "Bob", "grade": 72},
{"name": "Carol", "grade": 91},
]
# Sort by grade
sorted_students = sorted(students, key=lambda s: s["grade"])
for s in sorted_students:
print(s["name"], s["grade"])Output:
Bob 72
Alice 85
Carol 91Lambda Inside a Function (Returning Lambda)
One common pattern: a function that returns a lambda. This lets you create customised functions on the fly.
def multiplier(n):
return lambda x: x * n # returns a lambda that multiplies by n
double = multiplier(2)
triple = multiplier(3)
times10 = multiplier(10)
print(double(5)) # 10
print(triple(5)) # 15
print(times10(5)) # 50Output:
10
15
50🌍 Real-world use: Sorting e-commerce products by price, name, or rating. Filtering customer lists. Transforming data in data science pipelines.
Lambda Limitations
Lambda can only hold one expression. It cannot contain:
- Multiple statements
if/elseblocks (only inline ternary expressions)- Loops
returnkeyword (the expression IS the return value)
# ✅ Inline if/else IS allowed
classify = lambda x: "positive" if x > 0 else "non-positive"
print(classify(5)) # positive
print(classify(-3)) # non-positiveCommon Beginner Mistakes with Lambda
Mistake 1: Using return inside lambda
# ❌ WRONG
f = lambda x: return x * 2 # SyntaxError
# ✅ CORRECT
f = lambda x: x * 2Mistake 2: Using lambda for complex logic
# ❌ Bad practice — hard to read
f = lambda x, y: x**2 + 2*x*y + y**2 if x > 0 and y > 0 else 0
# ✅ Better — use a regular function for clarity
def compute(x, y):
if x > 0 and y > 0:
return x**2 + 2*x*y + y**2
return 0💡 Rule of thumb: If the lambda is hard to read in one line, write a regular function instead.
What Is Recursion?
Recursion is when a function calls itself inside its own body to solve a smaller version of the same problem.
Real-world analogy: Imagine you're in a long line at a cinema and you can't see the front. You ask the person in front of you, "What number are you in line?" They don't know either, so they ask the person in front of them, and so on. Eventually someone at the front says "I'm number 1!" Then the answer travels back: "I'm number 2", "I'm number 3", until the answer reaches you.
That chain of asking-then-reporting-back is recursion.
The Two Required Parts of Any Recursive Function
Every recursive function must have:
- Base case · the condition where the function stops calling itself (the "bottom of the line")
- Recursive case · where the function calls itself with a smaller/simpler input
Without a base case, the function calls itself forever → infinite recursion → Python crashes with a RecursionError.
Your First Recursive Function: Counting Down
def countdown(n):
if n <= 0: # Base case: stop here
print("Go!")
else:
print(n)
countdown(n - 1) # Recursive case: call with smaller n
countdown(5)Output:
5
4
3
2
1
Go!Let's trace the calls:
countdown(5) → prints 5, calls countdown(4)
countdown(4) → prints 4, calls countdown(3)
countdown(3) → prints 3, calls countdown(2)
countdown(2) → prints 2, calls countdown(1)
countdown(1) → prints 1, calls countdown(0)
countdown(0) → prints "Go!" — BASE CASE, stopsClassic Example: Factorial
The factorial of a number n (written n!) is:
5! = 5 × 4 × 3 × 2 × 1 = 120It has a natural recursive definition:
0! = 1(base case · defined mathematically)n! = n × (n-1)!(recursive case)
def factorial(n):
if n == 0: # Base case
return 1
else:
return n * factorial(n - 1) # Recursive case
print(factorial(5)) # 120
print(factorial(6)) # 720
print(factorial(0)) # 1Output:
120
720
1Tracing factorial(5):
factorial(5)
= 5 * factorial(4)
= 5 * 4 * factorial(3)
= 5 * 4 * 3 * factorial(2)
= 5 * 4 * 3 * 2 * factorial(1)
= 5 * 4 * 3 * 2 * 1 * factorial(0)
= 5 * 4 * 3 * 2 * 1 * 1
= 120Classic Example: Fibonacci Sequence
The Fibonacci sequence is: 0, 1, 1, 2, 3, 5, 8, 13, 21, …
Each number is the sum of the two before it.
fib(0) = 0(base case)fib(1) = 1(base case)fib(n) = fib(n-1) + fib(n-2)(recursive case)
def fib(n):
if n == 0:
return 0
elif n == 1:
return 1
else:
return fib(n - 1) + fib(n - 2)
for i in range(10):
print(f"fib({i}) = {fib(i)}")Output:
fib(0) = 0
fib(1) = 1
fib(2) = 1
fib(3) = 2
fib(4) = 3
fib(5) = 5
fib(6) = 8
fib(7) = 13
fib(8) = 21
fib(9) = 34Classic Example: Power Function
Calculate base ** exponent recursively:
def power(base, exp):
if exp == 0: # anything to the power 0 is 1
return 1
else:
return base * power(base, exp - 1)
print(power(2, 10)) # 1024
print(power(3, 4)) # 81Output:
1024
81Visualising the Call Stack
Every time a recursive function calls itself, Python pushes a new stack frame onto the call stack. When the base case is reached, the frames unwind in reverse order.
factorial(3) ← frame 1
factorial(2) ← frame 2
factorial(1) ← frame 3
factorial(0) ← frame 4 → returns 1
← returns 1 * 1 = 1
← returns 2 * 1 = 2
← returns 3 * 2 = 6Python has a default recursion limit of 1000 to prevent infinite recursion from crashing your system.
Recursion vs Loops
Both can solve the same problems. Here is the same task done two ways:
Loop version (sum 1 to n):
def sum_loop(n):
total = 0
for i in range(1, n + 1):
total += i
return total
print(sum_loop(5)) # 15Recursive version:
def sum_recursive(n):
if n == 0:
return 0
return n + sum_recursive(n - 1)
print(sum_recursive(5)) # 15Output:
15🌍 Real-world use: Recursion shines in tasks with naturally nested structure: file systems (folders inside folders), HTML/XML trees, organisation charts, mathematical proofs, sorting algorithms (merge sort, quicksort), and graph traversal (GPS routing, social networks).
Common Beginner Mistakes with Recursion
Mistake 1: Missing base case (infinite recursion)
# ❌ WRONG — never stops
def countdown(n):
print(n)
countdown(n - 1) # No base case → RecursionError
# ✅ CORRECT
def countdown(n):
if n <= 0: # base case!
return
print(n)
countdown(n - 1)Mistake 2: Base case never reached
# ❌ WRONG — n grows instead of shrinks
def countdown(n):
if n == 0:
return
countdown(n + 1) # Wrong direction — never reaches 0
# ✅ CORRECT — move toward the base case
def countdown(n):
if n == 0:
return
countdown(n - 1)Mistake 3: Forgetting to return the recursive result
# ❌ WRONG
def factorial(n):
if n == 0:
return 1
n * factorial(n - 1) # result computed but not returned!
# ✅ CORRECT
def factorial(n):
if n == 0:
return 1
return n * factorial(n - 1)🤔 Thinking prompt: What is the largest number you can pass to
factorial()before Python raises a RecursionError? Try it!
What Is a Generator?
Imagine you need to process 10 million numbers. If you put them all in a list, Python stores all 10 million in memory at once · that's enormous. A generator produces values one at a time, on demand, without storing them all in memory.
Real-world analogy: A list is like printing a whole book before reading it. A generator is like a printing press that prints one page at a time as you read.
Key properties:
- Defined like a function, but uses
yieldinstead ofreturn - Returns a generator object
- Values are produced lazily · only when asked for
- Once a value is yielded, the function pauses there and resumes next time
yield vs return
return | yield |
|---|---|
| Returns a value and the function ends | Returns a value and the function pauses |
| Called once | Can be called many times |
| Produces one result | Produces a series of results |
| Regular function | Generator function |
Your First Generator
def my_generator():
yield 1
yield 2
yield 3
gen = my_generator() # creates a generator object (nothing runs yet)
print(next(gen)) # runs until first yield → 1
print(next(gen)) # resumes, runs until second yield → 2
print(next(gen)) # resumes, runs until third yield → 3Output:
1
2
3What happens if you call next() again?
print(next(gen)) # StopIteration error — no more valuesLooping Over a Generator
The most common way to consume a generator is with a for loop. Python calls next() automatically and stops when StopIteration is raised.
def count_up(n):
i = 1
while i <= n:
yield i
i += 1
for number in count_up(5):
print(number)Output:
1
2
3
4
5Generator with an Infinite Sequence
Generators are perfect for sequences that go on forever · something you cannot do with a list.
def infinite_counter(start=0):
n = start
while True: # runs forever
yield n
n += 1
counter = infinite_counter(10)
for _ in range(5):
print(next(counter))Output:
10
11
12
13
14💡 No list of infinite length could ever exist · but a generator can represent one.
Generator Expressions (One-Liners)
Just as list comprehensions build lists in one line, generator expressions build generators in one line. Use parentheses () instead of square brackets [].
# List comprehension — creates ALL values in memory NOW
squares_list = [x**2 for x in range(10)]
# Generator expression — creates values one at a time on demand
squares_gen = (x**2 for x in range(10))
print(squares_list) # [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
print(squares_gen) # <generator object <genexpr> at 0x...>
for val in squares_gen:
print(val, end=" ")Output:
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
0 1 4 9 16 25 36 49 64 81Memory Comparison: List vs Generator
import sys
nums_list = [x for x in range(1000000)]
nums_gen = (x for x in range(1000000))
print("List size:", sys.getsizeof(nums_list), "bytes")
print("Generator size:", sys.getsizeof(nums_gen), "bytes")Output (approximate):
List size: 8056624 bytes (~8 MB)
Generator size: 104 bytes (~0.0001 MB)The generator uses almost no memory regardless of how many values it will produce.
A Practical Generator: Reading a Large File Line by Line
def read_large_file(filepath):
with open(filepath, "r") as f:
for line in f:
yield line.strip()
# Usage — only one line in memory at a time
for line in read_large_file("huge_log_file.txt"):
print(line)🌍 Real-world use: Log analysis, streaming database results, processing CSV files too large to fit in RAM, real-time sensor data, network packet processing.
Generator with Multiple yield Statements
def weather_report():
yield "Morning: Sunny, 18°C"
yield "Noon: Partly cloudy, 24°C"
yield "Evening: Windy, 20°C"
yield "Night: Clear, 15°C"
for report in weather_report():
print(report)Output:
Morning: Sunny, 18°C
Noon: Partly cloudy, 24°C
Evening: Windy, 20°C
Night: Clear, 15°CThe send() Method · Two-Way Communication
Generators can also receive values while running using .send().
def accumulator():
total = 0
while True:
value = yield total # yields total, then receives a new value
total += value
acc = accumulator()
next(acc) # prime the generator (runs to first yield)
print(acc.send(10)) # sends 10 → total = 10
print(acc.send(20)) # sends 20 → total = 30
print(acc.send(5)) # sends 5 → total = 35Output:
10
30
35Common Beginner Mistakes with Generators
Mistake 1: Trying to index a generator like a list
gen = (x for x in range(10))
# ❌ WRONG
print(gen[0]) # TypeError: 'generator' object is not subscriptable
# ✅ CORRECT — convert to list first if you need indexing
lst = list(gen)
print(lst[0]) # 0Mistake 2: Consuming a generator twice
gen = (x for x in range(5))
list1 = list(gen) # consumes all values
list2 = list(gen) # generator is exhausted — empty!
print(list1) # [0, 1, 2, 3, 4]
print(list2) # []Mistake 3: Confusing yield with return
# ❌ Using return — gives only ONE value, function ends
def give_numbers():
return 1
return 2 # never reached
# ✅ Using yield — gives multiple values, function pauses
def give_numbers():
yield 1
yield 2Exercise 1 · Decorator: Logging Function Calls
Objective: Write a decorator that logs when a function starts and ends.
Scenario: You are building a data processing pipeline and need to track which functions run and when.
Steps:
- Write a decorator called
log_call - It should print
"Calling: [function name]"before the function runs - It should print
"Finished: [function name]"after it finishes - Decorate a function called
process_data(items)that prints each item
Hint: Use func.__name__ to get the function's name.
Solution:
def log_call(func):
def wrapper(*args, **kwargs):
print(f"Calling: {func.__name__}")
result = func(*args, **kwargs)
print(f"Finished: {func.__name__}")
return result
return wrapper
@log_call
def process_data(items):
for item in items:
print(f" Processing: {item}")
process_data(["order_1", "order_2", "order_3"])Expected Output:
Calling: process_data
Processing: order_1
Processing: order_2
Processing: order_3
Finished: process_dataSelf-check questions:
- What would happen if you forgot
args, *kwargsin the wrapper? - How would you modify the decorator to also log the arguments passed?
Exercise 2 · Lambda: Sorting Products
Objective: Use lambda to sort a list of product dictionaries.
Scenario: An online shop needs to display products sorted by price (cheapest first), then by rating (highest first).
products = [
{"name": "Keyboard", "price": 49.99, "rating": 4.5},
{"name": "Mouse", "price": 29.99, "rating": 4.8},
{"name": "Monitor", "price": 299.99, "rating": 4.2},
{"name": "Headphones","price": 79.99, "rating": 4.7},
{"name": "Webcam", "price": 49.99, "rating": 4.3},
]
# Sort by price ascending
by_price = sorted(products, key=lambda p: p["price"])
# Sort by rating descending
by_rating = sorted(products, key=lambda p: p["rating"], reverse=True)
print("Sorted by price:")
for p in by_price:
print(f" {p['name']}: £{p['price']}")
print("\nSorted by rating:")
for p in by_rating:
print(f" {p['name']}: {p['rating']} stars")Expected Output:
Sorted by price:
Mouse: £29.99
Keyboard: £49.99
Webcam: £49.99
Headphones: £79.99
Monitor: £299.99
Sorted by rating:
Mouse: 4.8 stars
Headphones: 4.7 stars
Keyboard: 4.5 stars
Webcam: 4.3 stars
Monitor: 4.2 starsWhat-if challenge: Add a third sort: cheapest AND highest-rated first. Hint: key=lambda p: (p["price"], -p["rating"])
Exercise 3 · Recursion: Sum of Digits
Objective: Write a recursive function that sums all digits of a number.
Example: sum_digits(1234) → 1 + 2 + 3 + 4 = 10
Hint: A number's last digit is n % 10. The rest of the number is n // 10.
def sum_digits(n):
if n < 10: # Base case: single digit
return n
return (n % 10) + sum_digits(n // 10) # last digit + recurse on the rest
print(sum_digits(1234)) # 10
print(sum_digits(9999)) # 36
print(sum_digits(7)) # 7Expected Output:
10
36
7Self-check: Trace sum_digits(456) step by step on paper.
Exercise 4 · Generator: Student Grade Report
Objective: Write a generator that yields student report lines one at a time.
Scenario: A school system needs to print student reports without loading all 10,000 student records into memory at once.
students = [
{"name": "Alice", "score": 88},
{"name": "Bob", "score": 62},
{"name": "Carol", "score": 95},
{"name": "David", "score": 74},
{"name": "Eve", "score": 55},
]
def grade_report(student_list):
for student in student_list:
score = student["score"]
grade = "A" if score >= 90 else "B" if score >= 80 else "C" if score >= 70 else "D" if score >= 60 else "F"
yield f"{student['name']}: {score}/100 — Grade {grade}"
print("=== STUDENT GRADE REPORT ===")
for report_line in grade_report(students):
print(report_line)Expected Output:
=== STUDENT GRADE REPORT ===
Alice: 88/100 — Grade B
Bob: 62/100 — Grade D
Carol: 95/100 — Grade A
David: 74/100 — Grade C
Eve: 55/100 — Grade F| Mistake | Wrong | Correct |
|---|---|---|
| Calling function when passing to decorator | decorator(func()) | decorator(func) |
| Forgetting to return wrapper | def dec(f): def w(): f() | def dec(f): def w(): f(); return w |
return inside lambda | lambda x: return x*2 | lambda x: x*2 |
| Missing base case in recursion | No if to stop | Always include if n == base: return value |
| Wrong recursion direction | countdown(n+1) | countdown(n-1) |
| Indexing a generator | gen[0] | list(gen)[0] |
| Consuming a generator twice | list(gen); list(gen) | Create a new generator each time |
| Stacking decorators wrong order | Confusing inner/outer | Remember: bottom @ applies first |
In this project you will combine all four concepts · decorators, lambda, recursion, and generators · to build a simple budget analysis tool.
Project Overview
Scenario: You are building a personal finance tool. Given a list of transactions, you need to:
- Log every major function call (decorator)
- Filter and sort transactions (lambda)
- Calculate running totals recursively (recursion)
- Stream the report line by line (generator)
Stage 1 · Setup and Decorator
# Stage 1: Logging decorator
def log_operation(func):
def wrapper(*args, **kwargs):
print(f"\n[LOG] Running: {func.__name__}")
result = func(*args, **kwargs)
print(f"[LOG] Completed: {func.__name__}")
return result
return wrapperLesson 16 complete! 🎉
You covered:
- ✅ Lesson Introduction
- ✅ Prerequisite Concepts
- ✅ Part 1 · Python Decorators
- ✅ Part 2 · Lambda Functions
- ✅ Part 3 · Python Recursion
- ✅ Part 4 · Python Generators
- ✅ Guided Practice Exercises
- ✅ Common Beginner Mistakes · Consolidated Reference