> ## Documentation Index
> Fetch the complete documentation index at: https://fastapi2day.codewithsiva.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Advanced Functions: Lambdas, Closures & Decorators

> Master Python's advanced function features — first-class objects, lambdas, *args/**kwargs, closures, and custom decorator patterns.

Python's multi-paradigm nature lets you treat functions as first-class citizens: you can store them in variables, pass them into other functions, return them as values, and inspect their attributes. This goes far beyond simple code reuse — it unlocks powerful patterns like closures (functions that remember their creation environment) and decorators (wrappers that extend behaviour without touching the original code). This page covers every advanced function concept you need before diving into FastAPI and modern Python libraries.

## Functions as First-Class Objects

In Python, a function is an object in memory with a type, an identity, and a value. You can manipulate it like any other value:

```python theme={null}
def greet(name):
    return f"Hello, {name}!"

# 1. Assign to another variable
say_hello = greet
print(say_hello("Aarav"))   # Hello, Aarav!

# 2. Store in a list
def shout(name):
    return f"HELLO, {name}!"

actions = [greet, shout]
for action in actions:
    print(action("Amit"))
# Hello, Amit!
# HELLO, Amit!

# 3. Pass as an argument
def run_action(name, action_func):
    return action_func(name)

print(run_action("Dev", greet))   # Hello, Dev!

# 4. Inspect built-in attributes
print(greet.__name__)   # greet
print(id(greet))        # memory address
```

## Lambda Functions

A **lambda** is a small, anonymous function defined with a single expression. No `return` keyword is needed — the expression is evaluated and returned automatically.

```python theme={null}
# Standard function
def add(x, y):
    return x + y

# Lambda equivalent
add_lambda = lambda x, y: x + y
print(add_lambda(3, 7))   # 10
```

### Real-World Lambda Example

```python theme={null}
prices = [100, 250, 500]
prices_with_gst = list(map(lambda price: price * 1.18, prices))
print(prices_with_gst)   # [118.0, 295.0, 590.0]
```

### Multi-line Lambda Expressions (Nested Conditionals)

You can wrap a single expression across multiple lines using parentheses:

```python theme={null}
classify_score = lambda score: (
    "Excellent" if score >= 90 else
    "Good"      if score >= 75 else
    "Pass"      if score >= 50 else
    "Fail"
)

print(classify_score(82))   # Good
print(classify_score(45))   # Fail
```

<Warning>
  Complex nested conditionals inside a lambda hurt readability. If your logic spans multiple lines or requires multiple statements (assignments, loops, print calls), use a normal `def` function.
</Warning>

<Tip>
  Lambdas shine as concise inline arguments to higher-order functions:

  ```python theme={null}
  points = [(1, 2), (3, 1), (5, 0)]
  sorted_points = sorted(points, key=lambda p: p[1])
  print(sorted_points)   # [(5, 0), (3, 1), (1, 2)]
  ```
</Tip>

## Variable-Length Arguments (`*args` and `**kwargs`)

Accept an arbitrary number of arguments using starred parameters:

```python theme={null}
def print_everything(*args, **kwargs):
    print("Positional args:", args)
    print("Keyword args:  ", kwargs)

print_everything(1, 2, 3, name="Alice", age=25)
# Positional args: (1, 2, 3)
# Keyword args:   {'name': 'Alice', 'age': 25}
```

* `*args` collects extra positional arguments into a **tuple**.
* `**kwargs` collects extra keyword arguments into a **dictionary**.

## Closures

A **closure** is a nested function that retains access to variables from its enclosing function's scope — even after the outer function has finished executing.

Three conditions must hold:

1. There is a nested (inner) function.
2. The inner function references a variable from the outer scope.
3. The outer function **returns** the inner function.

```python theme={null}
def make_multiplier(factor):
    def multiplier(number):
        return number * factor   # 'factor' is captured from outer scope
    return multiplier

double = make_multiplier(2)
triple = make_multiplier(3)

print(double(10))   # 20
print(triple(10))   # 30
```

Each call to `make_multiplier()` creates an independent closure with its own `factor` value — `double` and `triple` are completely separate functions.

## Decorators

A **decorator** is a higher-order function that wraps another function to add behaviour before, after, or around the original call — without modifying its source code.

### Writing a Basic Decorator

```python theme={null}
def my_decorator(func):
    def wrapper():
        print("Something is happening before the function is called.")
        func()
        print("Something is happening after the function is called.")
    return wrapper

@my_decorator
def say_hello():
    print("Hello!")

say_hello()
# Something is happening before the function is called.
# Hello!
# Something is happening after the function is called.
```

The `@my_decorator` syntax is shorthand for `say_hello = my_decorator(say_hello)`.

### Decorating Functions with Arguments

Use `*args` and `**kwargs` in the wrapper so the decorator works with **any** function signature:

```python theme={null}
def log_arguments(func):
    def wrapper(*args, **kwargs):
        print(f"Calling {func.__name__} with args={args}, kwargs={kwargs}")
        result = func(*args, **kwargs)
        print(f"{func.__name__} returned: {result}")
        return result
    return wrapper

@log_arguments
def add_numbers(a, b):
    return a + b

add_numbers(10, 20)
# Calling add_numbers with args=(10, 20), kwargs={}
# add_numbers returned: 30
```

### Practical Decorator Example — Timing

```python theme={null}
import time

def timer(func):
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        elapsed = time.time() - start
        print(f"{func.__name__} took {elapsed:.4f}s")
        return result
    return wrapper

@timer
def slow_operation():
    time.sleep(0.1)
    return "done"

print(slow_operation())
# slow_operation took 0.1002s
# done
```

<Note>
  For production decorators, wrap the inner function with `functools.wraps(func)` to preserve the original function's `__name__`, `__doc__`, and other metadata:

  ```python theme={null}
  from functools import wraps

  def my_decorator(func):
      @wraps(func)
      def wrapper(*args, **kwargs):
          return func(*args, **kwargs)
      return wrapper
  ```
</Note>
