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

# Functions

> Create reusable blocks of code, pass arguments, manage scope, and get results

## 1. Defining and Calling Functions

A function is a named block of code that performs a specific task. You define it once, then call it whenever you need that task done.

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

# Call the function
greet()
```

### Function Syntax

Every function follows this pattern:

```python theme={null}
def function_name():
    # Code goes here
    # Must be indented
    pass
```

Key parts:

* `def`: keyword that creates a function.
* **Function name** followed by parentheses `()`.
* **Colon** `:` to start the function body.
* **Indented code block** (the function body).

### Naming Functions

Follow these rules for function names (Snake Case):

* Use lowercase letters.
* Separate words with underscores.
* Be descriptive about what the function does.

```python theme={null}
# Good names
def calculate_total():
    pass

# Bad names
def func1():  # Not descriptive
    pass

def Calculate():  # Should be lowercase
    pass
```

***

## 2. Parameters & Arguments

Parameters let you pass data into functions. Instead of hardcoding values, you make functions flexible to work with different inputs.

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

greet("Alice")
greet("Bob")
```

> \[!NOTE]
> The variables in the function definition are **parameters**. The actual values you pass when calling the function are **arguments**.

### Positional Arguments

By default, Python matches the arguments you pass to the parameters in the definition by their **position** (order).

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

# "hamster" maps to animal_type, "Harry" to pet_name
describe_pet("hamster", "Harry")  # I have a hamster named Harry.
```

### Default Values

You can 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 at the end of the parameter list.

### Keyword Arguments

You can call functions using parameter names for clarity, which allows you to pass them 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)
```

***

## 3. Return Values

Use the `return` statement to send a value back from a function to the caller.

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

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

> \[!IMPORTANT]
> When Python encounters a `return` statement, it immediately exits the function. Any code after the `return` statement will not execute.

### Returning Multiple Values

You can return multiple values from a function by separating them 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
```

***

## 4. Flexible & Enforced Arguments

Python provides advanced options for handling dynamic numbers of arguments and enforcing calling styles.

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

Prefix a parameter name with a single asterisk `*` to accept any number of positional arguments. Inside the function, `args` is 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
```

### Variable Keyword Arguments (`**kwargs`)

Prefix a parameter name with a double asterisk `**` to accept any number of keyword arguments. Inside the function, `kwargs` is 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")
```

### Argument Ordering Rules

When mixing argument types, you must define them in this exact order:

1. Standard positional arguments
2. `*args`
3. Keyword-only arguments
4. `**kwargs`

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

* **Positional-Only** (`/`): Parameters before `/` must be passed positionally.
* **Keyword-Only** (`*`): Parameters after `*` must be passed as keyword arguments.

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

***

## 5. Variable Scope & LEGB Rule

The **scope** of a variable refers to the region of a program where that variable is accessible.

### LEGB Lookup Order

When you reference a variable name, Python searches for it in this strict order:

1. **L**ocal: Inside the current function.
2. **E**nclosing: Inside any enclosing (outer) functions.
3. **G**lobal: Top-level variables defined in the module/file.
4. **B**uilt-in: Python's pre-loaded functions (like `len`, `print`).

If not found, Python raises a `NameError`.

```python theme={null}
# Global variable
x = "global"

def outer():
    # Enclosing variable
    x = "enclosing"
    
    def inner():
        # Local variable
        x = "local"
        print(x) # Prints "local"
        
    inner()

outer()
```

### The `global` and `nonlocal` Keywords

* **`global`**: Declares that a variable inside a function refers to the global scope, allowing you to modify it.
* **`nonlocal`**: Declares that a variable inside a nested function refers to the enclosing (outer) scope, allowing you to modify it.

```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)  # Prints: Hello from Inner!
```

Python treats **functions as first-class objects**, meaning they can be assigned to variables, passed as arguments, returned from other functions, and stored in collections.

***

## Functions as First-Class Objects

Functions can be:

* Assigned to variables.
* Passed as arguments.
* Returned from functions.
* Accessed using attributes like `__name__`.

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

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

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

print(execute(greet, "Bob"))

print(greet.__name__)
```

***

## Lambda Functions

A **lambda** is a small anonymous function consisting of a single expression.

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

# Lambda function
add_lambda = lambda x, y: x + y

print(add_lambda(3, 7))
```

Lambdas are commonly used with functions like `map()`, `filter()`, and `sorted()`.

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

squares = list(map(lambda x: x**2, numbers))
print(squares)
```

```python theme={null}
points = [(1, 2), (3, 1), (5, 0)]

sorted_points = sorted(points, key=lambda p: p[1])
print(sorted_points)
```

<Warning>
  Use lambda functions only for simple expressions. For complex logic, use a normal function.
</Warning>

***

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

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

```python theme={null}
def display(*args, **kwargs):
    print(args)
    print(kwargs)

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

***

## Closures

A **closure** is a nested function that remembers variables from its enclosing scope.

```python theme={null}
def make_multiplier(factor):

    def multiply(number):
        return number * factor

    return multiply


double = make_multiplier(2)

print(double(10))
```

***

## Decorators

A **decorator** extends the behavior of a function without modifying its source code.

```python theme={null}
def my_decorator(func):

    def wrapper():
        print("Before")
        func()
        print("After")

    return wrapper


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


greet()
```

### Decorators with Arguments

Use `*args` and `**kwargs` to support functions with any number of arguments.

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

Python supports **Functional Programming (FP)**, where functions are treated as first-class objects. In this paradigm, functions can be passed as arguments, returned from other functions, and used to build reusable and declarative code.

Functional programming in Python is mainly based on:

* **Declarative Programming**
* **Higher-Order Functions**

## Generators

A **generator** is a special type of function that produces values **one at a time** instead of returning them all at once. It uses the `yield` keyword instead of `return`. Generators are **memory-efficient** because they generate values only when needed.

### Advantages

* Uses less memory for large datasets.
* Produces values lazily (on demand).
* Suitable for processing streams of data or large files.

### Creating a Generator

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

numbers = count_up_to(5)

for num in numbers:
    print(num)
```

**Output**

```
1
2
3
4
5
```

### Using `next()`

A generator can be iterated manually using the `next()` function.

```python theme={null}
def countdown():
    yield 3
    yield 2
    yield 1

gen = countdown()

print(next(gen))
print(next(gen))
print(next(gen))
```

**Output**

```
3
2
1
```

### Generator Expression

Similar to list comprehensions, Python supports **generator expressions**.

```python theme={null}
squares = (x ** 2 for x in range(5))

for square in squares:
    print(square)
```

**Output**

```
0
1
4
9
16
```

### Generator vs List

| List                        | Generator                      |
| --------------------------- | ------------------------------ |
| Stores all values in memory | Generates values one at a time |
| Uses more memory            | Memory efficient               |
| Created using `[]`          | Created using `yield` or `()`  |
| Faster for small datasets   | Better for large datasets      |

***

## Declarative Programming

Declarative programming focuses on **what** you want to achieve, while imperative programming focuses on **how** to achieve it.

### Imperative vs Declarative

**Imperative (How):**

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

doubled = []

for num in numbers:
    doubled.append(num * 2)

print(doubled)
```

**Declarative (What):**

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

doubled = list(map(lambda x: x * 2, numbers))

print(doubled)
```

***

## Higher-Order Functions

A **Higher-Order Function** is a function that:

* Accepts one or more functions as arguments.
* Returns a function as its result.

### Passing Functions as Arguments

```python theme={null}
def process_numbers(numbers, operation):
    return [operation(num) for num in numbers]


def square(x):
    return x * x


def cube(x):
    return x ** 3


nums = [1, 2, 3, 4]

print(process_numbers(nums, square))
print(process_numbers(nums, cube))
print(process_numbers(nums, lambda x: x + 10))
```

### Returning Functions

```python theme={null}
def multiplier(factor):

    def multiply(number):
        return number * factor

    return multiply


double = multiplier(2)
triple = multiplier(3)

print(double(10))
print(triple(10))
```

### Real-World Example

```python theme={null}
def process_salary(salary, policy):
    return policy(salary)


def tax(salary):
    return salary * 0.9


def bonus(salary):
    return salary * 1.2


salary = 50000

print(process_salary(salary, tax))
print(process_salary(salary, bonus))
```

***

## Built-in Higher-Order Functions

### `map()`

Applies a function to every element of an iterable.

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

squared = list(map(lambda x: x**2, numbers))

print(squared)
```

You can also pass a normal function when the logic is more complex.

```python theme={null}
def get_grade(marks):
    if marks >= 90:
        return "A"
    elif marks >= 75:
        return "B"
    elif marks >= 50:
        return "C"
    return "F"


student_marks = [88, 45, 92, 67]

grades = list(map(get_grade, student_marks))

print(grades)
```

***

### `filter()`

Returns only the elements that satisfy a condition.

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

even_numbers = list(filter(lambda x: x % 2 == 0, numbers))

print(even_numbers)
```

***

### `reduce()`

The `reduce()` function (from the `functools` module) combines all elements into a single value.

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

numbers = [1, 2, 3, 4]

product = reduce(lambda x, y: x * y, numbers)

print(product)
```

***

## Functional Pipeline Example

The following example combines `filter()`, `map()`, and `reduce()` to compute the **sum of squares of even numbers**.

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

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

evens = filter(lambda x: x % 2 == 0, numbers)
squares = map(lambda x: x**2, evens)

total = reduce(lambda x, y: x + y, squares)

print(total)
```

The same pipeline can also be written in a single statement.

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

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

total = reduce(
    lambda x, y: x + y,
    map(lambda x: x**2,
        filter(lambda x: x % 2 == 0, numbers))
)

print(total)
```

***

## Pythonic Alternative

Although `map()`, `filter()`, and `reduce()` are useful, Python often provides a simpler and more readable solution using comprehensions and built-in functions.

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

total = sum(x**2 for x in numbers if x % 2 == 0)

print(total)
```

<Tip>
  Use **list comprehensions** and built-in functions like `sum()` for better readability. Use `map()`, `filter()`, and `reduce()` when working with functional pipelines or callback-based APIs.
</Tip>

***

## Practice & Exercises

To reinforce what you've learned in this section (defining functions, scopes, lambda expressions, closures, decorators, and functional programming), practice with these interactive notebooks:

<CardGroup cols={2}>
  <Card title="Follow-Along Practice" icon="laptop-code">
    Practice function definitions, positional/keyword arguments, variable scope rules, closures, wrapper decorators, and higher-order functions (map, filter, reduce).

    [💻 VS Code](vscode://file/Users/sivaprasad/Downloads/python%20material/fastapi-course/public/notebooks/basics_exercises/Functions_Practice.ipynb) | [🚀 Colab](https://colab.research.google.com/github/prasad230776/fastapi-course/blob/master/public/notebooks/basics_exercises/Functions_Practice.ipynb) | <a href="/public/notebooks/basics_exercises/Functions_Practice.ipynb" download>📥 Download</a>
  </Card>

  <Card title="Practice Exercises" icon="pen-to-square">
    Test your knowledge with hands-on exercises including positional/keyword argument formatters, nonlocal scope counters, closure factories, execution timer decorators, and functional pipelines.

    [💻 VS Code](vscode://file/Users/sivaprasad/Downloads/python%20material/fastapi-course/public/notebooks/basics_exercises/Functions_Exercises.ipynb) | [🚀 Colab](https://colab.research.google.com/github/prasad230776/fastapi-course/blob/master/public/notebooks/basics_exercises/Functions_Exercises.ipynb) | <a href="/public/notebooks/basics_exercises/Functions_Exercises.ipynb" download>📥 Download</a>
  </Card>
</CardGroup>
