Skip to Content

Functions, Decorators & Lambdas

When to use

Name reusable behaviour; wrap with cross-cutting concerns; pass a small inline callback.

Analogy

A function is a recipe card; decorator is a laminator that adds ‘gluten-free’ or ‘spicy’ to the card; lambda is a sticky note for a one-liner.

Data-flow diagram

  @trace
  def add(a,b): return a+b

  becomes: add = trace(add)
  call add(2,3) -> wrapper prints CALL -> add(2,3) -> 5

Deep explanation

Functions are first-class: assignable, passable, returnable. Decorators wrap a function (stacking @a @b def f applies bottom-up). @functools.wraps(func) preserves __name__/__doc__ - always use it. Lambdas are restricted to one expression for throwaway callbacks.

Examples

Example 1

def greet(name):
    return f'Hello, {name}'
print(greet('Ada'))

simplest def; positional arg.

Example 2

import functools
def trace(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print(f'CALL {func.__name__}')
        return func(*args, **kwargs)
    return wrapper

@trace
def add(a, b): return a + b

decorator with *args, **kwargs; @functools.wraps keeps metadata.

Example 3

tickets = [('hi',1),('low',5),('med',3)]
print(sorted(tickets, key=lambda t: t[1]))

lambda as standard key= callback.

Common mistake

Writing a decorator without @functools.wraps(func) - the wrapped function loses its __name__, breaking debuggers, Sentry fingerprinting, OpenAPI schemas. Always @wraps(func).

Key takeaway

Default to def; lambdas only for key=/value= callbacks; @functools.wraps on every decorator; prefer *args, **kwargs forwarding.

Production Failure Playbook

Failure scenario 1: decorator-without-wraps

Failure scenario 2: lambda-capturing-loop-var