Lambdas, decorators, and generators are three power tools that show up in real Python code. Lambdas create small anonymous functions. Decorators wrap a function to add behaviour like timing, logging, or caching. Generators produce values lazily and can model infinite sequences.
Lambda functions
A lambda is a single-expression anonymous function. Use it when you need a tiny function for one specific call site, especially as the key argument to sorted or inside map, filter, and reduce. Avoid lambdas that span multiple expressions.
# Sort a list of tuples by the second element
pairs = [("apple", 3), ("banana", 1), ("cherry", 2)]
pairs.sort(key=lambda p: p[1])
print(pairs) # [('banana', 1), ('cherry', 2), ('apple', 3)]
# Use map and filter with a lambda
nums = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
evens = list(filter(lambda n: n % 2 == 0, nums))
squares = list(map(lambda n: n * n, nums))
print(evens) # [2, 4, 6, 8, 10]
print(squares) # [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
A decorator is a function that wraps another function
A decorator takes a function, returns a new function that usually calls the original with extra behaviour. The @decorator syntax is shorthand for passing the function to the decorator and rebinding the name.
import functools, time
def timer(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
elapsed = time.perf_counter() - start
print(f"{func.__name__} took {elapsed:.4f}s")
return result
return wrapper
@timer
def slow_sum(n):
return sum(range(n))
slow_sum(10_000_000) # slow_sum took 0.1234s
Decorators that take arguments
A decorator that takes arguments is a function that returns a decorator. Three nested functions, usually. functools.wraps preserves the wrapped function's name and docstring so introspection still works.
import functools
def repeat(times):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
results = []
for _ in range(times):
results.append(func(*args, **kwargs))
return results
return wrapper
return decorator
@repeat(3)
def greet(name):
return f"Hello, {name}"
print(greet("Asha")) # ['Hello, Asha', 'Hello, Asha', 'Hello, Asha']
Generators with yield
A function that contains the yield keyword is a generator. Calling it returns a generator object that produces values lazily, one per next() call. Generators are memory-efficient for large or infinite sequences.
def countdown(n):
while n > 0:
yield n
n -= 1
for x in countdown(5):
print(x, end=" ") # 5 4 3 2 1
print()
# Generator expression, like a list comprehension with parens
sum_of_squares = sum(n * n for n in range(10))
print(sum_of_squares) # 285
Sending values into a generator
Beyond next(), you can send a value back into a generator with the send method. The value becomes the result of the current yield expression. This is how coroutines and pipelines are built in older Python code.
def running_average():
total = 0
count = 0
while True:
value = yield total / count if count else 0
total += value
count += 1
avg = running_average()
next(avg) # prime the generator
print(avg.send(10)) # 10.0
print(avg.send(20)) # 15.0
print(avg.send(30)) # 20.0
functools.cache and the lru_cache decorator
For functions that are called many times with the same arguments, functools.cache or functools.lru_cache memoizes the results. Pure functions with hashable arguments are perfect candidates. Use cache for unbounded growth and lru_cache(maxsize=N) for bounded memory.
import functools
@functools.cache
def fib(n):
if n < 2:
return n
return fib(n - 1) + fib(n - 2)
print(fib(100)) # 354224848179261915075
Common pitfalls with these tools
Lambdas, decorators, and generators each have a sharp edge. Lambdas are restricted to a single expression, so the moment you reach for an assignment or a statement inside one, stop and write a def. Many languages call single-expression functions "anonymous", which signals that they should be small. Decorators that return the wrapper without functools.wraps hide the original function's __name__ and __doc__, which breaks help(), logging, and tests that introspect signatures. Always decorate the wrapper with functools.wraps. Generators are single-use: once exhausted, calling next again raises StopIteration. If the data should be replayed, return a list or write a class with __iter__. Also, generators swallow exceptions in surprising ways if you send a value after they finish, so always pair them with explicit lifecycle handling.
Comparison with JavaScript and C++
Python lambdas correspond to JavaScript arrow functions but with the expression-only restriction. Where JavaScript lets you write (x) => { return x * 2; } with statements inside the body, Python forces you to use def for any non-trivial body. C++ has no direct lambda equivalent at the language level, but std::function plus a capture list gives similar behaviour for closures. Python decorators are essentially JavaScript higher-order functions, but the @syntax is more compact. C++ does not have decorators; the closest analogue is a wrapper function you call manually. Generators are unique to Python among these languages; JavaScript gets generators only with function* and yield, and C++ uses coroutines (C++20) which are similar in spirit but very different in mechanism. Treat generators as a Python superpower: most other languages would force you to build a state machine by hand.
Best practices for lambdas, decorators, and generators
Three habits keep these tools readable. First, lambdas should fit on one line and use clear names for their arguments, even though the language does not require named parameters. lambda r, c: r * c is better than lambda x, y: x * y when the inputs are rows and columns. Second, write decorators in a library module so you can reuse them across projects, and document the order in which multiple decorators apply (top-down, like layers of an onion). Third, prefer generator expressions over list comprehensions when the sequence is consumed once and can be large, since generator expressions hold no intermediate list. Combined with itertools.islice, chain, and accumulate, they let you build lazy pipelines that look like SQL queries and behave like database cursors.
Summary
Lambdas for short throwaway functions. Decorators to wrap behaviour around existing functions. Generators to compute values one at a time without holding the whole sequence in memory. These three tools combined with the standard library's functools make Python code expressive and fast. The next lesson covers strings, formatting, and the powerful str methods.