> ## 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.

# Python Functions: Parameters, Scope, Closures & Decorators

> Define reusable functions, master every argument type, understand LEGB scope rules, and write closures, decorators, and generators.

Functions are the fundamental building blocks of every Python program. A function is a named, reusable block of code that performs a specific task — you define it once and call it as many times as you need with different inputs. Beyond simple reuse, Python treats functions as first-class objects, meaning you can assign them to variables, pass them as arguments, and return them from other functions. This page covers everything from basic definitions to closures, decorators, and functional programming patterns.

## Defining and Calling Functions

Use the `def` keyword to define a function, followed by a name, parentheses, and a colon. The indented block beneath is the function body.

```python theme={null}
def greet():
    print("Hello, world!")
    print("Welcome to Python!")

greet()
```

### Naming Conventions

Follow these rules for clear, Pythonic function names (snake\_case):

```python theme={null}
# Good — descriptive, lowercase, snake_case
def calculate_total():
    pass

# Bad — not descriptive
def func1():
    pass

# Bad — should be lowercase
def Calculate():
    pass
```

## Parameters and Arguments

Parameters make functions flexible by accepting input values:

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

greet("Alice")   # Hello, Alice!
greet("Bob")     # Hello, Bob!
```

<Note>
  The names in the **function definition** are called **parameters**. The actual values you pass when calling the function are called **arguments**.
</Note>

### Positional Arguments

By default, Python matches arguments to parameters by their **position**:

```python theme={null}
def describe_pet(animal_type, pet_name):
    print(f"I have a {animal_type} named {pet_name}.")

describe_pet("hamster", "Harry")
# I have a hamster named Harry.
```

### Default Values

Give parameters default values to make them optional:

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

greet("Alice")           # Hello, Alice!
greet("Bob", "Hi")       # Hi, Bob!
```

<Tip>
  Always put parameters with default values **after** parameters without defaults.
</Tip>

### Keyword Arguments

Pass arguments by name for clarity, in any order:

```python theme={null}
def create_profile(name, age, city):
    print(f"{name}, {age}, from {city}")

create_profile(city="New York", name="Alice", age=25)
```

## Return Values

Use `return` to send a value back to the caller. Python exits the function immediately when it hits `return`:

```python theme={null}
def add(a, b):
    return a + b

result = add(5, 3)
print(result)   # 8
```

### Returning Multiple Values

Separate values with commas — Python wraps them in a tuple automatically:

```python theme={null}
def get_min_max(numbers):
    return min(numbers), max(numbers)

minimum, maximum = get_min_max([5, 2, 8, 1, 9])
print(f"Min: {minimum}, Max: {maximum}")   # Min: 1, Max: 9
```

## Flexible Arguments

### `*args` — Variable Positional Arguments

Prefix a parameter with `*` to collect any number of positional arguments into a **tuple**:

```python theme={null}
def sum_all(*numbers):
    total = 0
    for num in numbers:
        total += num
    return total

print(sum_all(1, 2, 3))        # 6
print(sum_all(10, 20, 30, 40)) # 100
```

### `**kwargs` — Variable Keyword Arguments

Prefix a parameter with `**` to collect any number of keyword arguments into a **dictionary**:

```python theme={null}
def show_profile(**info):
    for key, value in info.items():
        print(f"{key}: {value}")

show_profile(name="Alice", age=25, city="NYC")
# name: Alice
# age: 25
# city: NYC
```

### Combined Example

```python theme={null}
def display(*args, **kwargs):
    print(args)    # (1, 2, 3)
    print(kwargs)  # {'name': 'Alice', 'age': 20}

display(1, 2, 3, name="Alice", age=20)
```

### Positional-Only `/` and Keyword-Only `*` Parameters

```python theme={null}
# 'first' and 'second' are positional-only; 'third' is keyword-only
def mix_example(first, second, /, *, third):
    print(first, second, third)

mix_example(1, 2, third=3)   # Correct
```

## Variable Scope & the LEGB Rule

**Scope** refers to which parts of your code can see a given variable. Python searches for variable names in this strict order:

<Steps>
  <Step title="L — Local">
    Variables defined inside the current function.
  </Step>

  <Step title="E — Enclosing">
    Variables in any surrounding (outer) function scopes.
  </Step>

  <Step title="G — Global">
    Variables defined at the top level of the module.
  </Step>

  <Step title="B — Built-in">
    Python's pre-loaded names like `len`, `print`, `range`.
  </Step>
</Steps>

```python theme={null}
x = "global"

def outer():
    x = "enclosing"

    def inner():
        x = "local"
        print(x)   # local

    inner()

outer()
```

### `global` and `nonlocal`

```python theme={null}
counter = 0

def increment_global():
    global counter
    counter += 1

def outer_func():
    message = "Hello"
    def inner_func():
        nonlocal message
        message = "Hello from Inner!"
    inner_func()
    print(message)   # Hello from Inner!
```

## Functions as First-Class Objects

Python functions are objects — you can assign them, pass them, and inspect their attributes:

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

# Assign to a variable
say_hello = greet
print(say_hello("Alice"))   # Hello, Alice!

# Pass as an argument
def execute(func, name):
    return func(name)

print(execute(greet, "Bob"))   # Hello, Bob!
print(greet.__name__)           # greet
```

## Lambda Functions

A **lambda** is a small, anonymous function that consists of a single expression:

```python theme={null}
add = lambda x, y: x + y
print(add(3, 7))   # 10
```

Lambdas are most useful as inline arguments to higher-order functions:

```python theme={null}
numbers = [1, 2, 3, 4]
squares = list(map(lambda x: x**2, numbers))
print(squares)   # [1, 4, 9, 16]

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)]
```

<Warning>
  Use lambda functions only for **simple, single-expression** logic. For anything more complex, write a normal `def` function instead.
</Warning>

## Closures

A **closure** is a nested function that remembers variables from its enclosing scope even after the outer function has finished:

```python theme={null}
def make_multiplier(factor):
    def multiply(number):
        return number * factor   # 'factor' is remembered
    return multiply

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

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

## Decorators

A **decorator** is a higher-order function that wraps another function to extend its behaviour without modifying its source:

```python theme={null}
def my_decorator(func):
    def wrapper():
        print("Before")
        func()
        print("After")
    return wrapper

@my_decorator
def greet():
    print("Hello")

greet()
# Before
# Hello
# After
```

### Decorators with Arguments

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

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

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

print(add(10, 20))
# Calling add
# 30
```

## Generators

A **generator** produces values one at a time using `yield`, consuming far less memory than returning a full list:

```python theme={null}
def count_up_to(n):
    for i in range(1, n + 1):
        yield i

for num in count_up_to(5):
    print(num)   # 1 2 3 4 5
```

Use `next()` to retrieve values manually:

```python theme={null}
gen = count_up_to(3)
print(next(gen))   # 1
print(next(gen))   # 2
print(next(gen))   # 3
```

**Generator expression** (lazy version of a list comprehension):

```python theme={null}
squares = (x ** 2 for x in range(5))
for s in squares:
    print(s)   # 0 1 4 9 16
```

## Higher-Order Functions

### `map()`, `filter()`, and `reduce()`

```python theme={null}
numbers = [1, 2, 3, 4, 5, 6]

# map — transform every element
squared = list(map(lambda x: x**2, numbers))

# filter — keep elements that match a condition
evens = list(filter(lambda x: x % 2 == 0, numbers))

# reduce — collapse a sequence to a single value
from functools import reduce
total = reduce(lambda x, y: x + y, numbers)

print(squared)   # [1, 4, 9, 16, 25, 36]
print(evens)     # [2, 4, 6]
print(total)     # 21
```

### Functional Pipeline

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

numbers = [1, 2, 3, 4, 5, 6]

# Sum of squares of even numbers
total = reduce(
    lambda x, y: x + y,
    map(lambda x: x**2,
        filter(lambda x: x % 2 == 0, numbers))
)
print(total)   # 56
```

<Tip>
  The same result is often more readable using a generator expression: `sum(x**2 for x in numbers if x % 2 == 0)`. Use `map`/`filter`/`reduce` for functional pipelines or callback-based APIs.
</Tip>
