> For the complete documentation index, see [llms.txt](https://py.d19.in/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://py.d19.in/chapter-4-functions.md).

# Chapter 4: Functions

Functions are reusable blocks of code designed to perform a specific task. They help organize code, reduce repetition, and improve readability and maintainability. In Python, functions are first-class objects, meaning they can be passed around as arguments, returned from other functions, and assigned to variables.

#### Defining and Calling Functions

A function must be defined before it can be called. Use the `def` keyword to define a function. Functions can take inputs, perform tasks, and optionally return outputs.

**Syntax:**

```python
def function_name(parameters):
    """Optional documentation string (docstring)"""
    # Code block
    return value  # Optional
```

**Example:**

```python
def greet(name):
    """This function returns a greeting message."""
    return f"Hello, {name}!"

# Calling the function
greeting = greet("Alice")
print(greeting)
```

**Key Points:**

* Function names should follow the same naming rules as variables.
* Parameters are optional; a function can take zero or more arguments.
* The `return` statement is optional but allows the function to send a result back to the caller.
* Include a docstring to describe the function’s purpose and behavior.

#### Parameters and Arguments

**Types of Parameters:**

1. **Positional Parameters:** Must be provided in the order they appear in the function definition.

   ```python
   def multiply(a, b):
       return a * b

   print(multiply(2, 3))  # Output: 6
   ```
2. **Default Parameters:** Provide default values for arguments, making them optional.

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

   greet()  # Output: Hello, Guest!
   greet("Alice")  # Output: Hello, Alice!
   ```
3. **Keyword Arguments:** Allow specifying arguments by name, regardless of their order.

   ```python
   def divide(numerator, denominator):
       return numerator / denominator

   print(divide(denominator=4, numerator=8))  # Output: 2.0
   ```
4. **Arbitrary Arguments:** Use `*args` for non-keyword variable-length arguments and `**kwargs` for keyword variable-length arguments.

   ```python
   def print_all(*args, **kwargs):
       print("Positional:", args)
       print("Keyword:", kwargs)

   print_all(1, 2, 3, a="apple", b="banana")
   # Positional: (1, 2, 3)
   # Keyword: {'a': 'apple', 'b': 'banana'}
   ```

#### Return Values

A function can return a value to the caller using the `return` statement. If no `return` is provided, the function implicitly returns `None`.

**Examples:**

```python
def square(num):
    return num ** 2

print(square(4))  # Output: 16

# Multiple return values

def stats(numbers):
    return min(numbers), max(numbers), sum(numbers) / len(numbers)

print(stats([1, 2, 3, 4, 5]))  # Output: (1, 5, 3.0)
```

#### Scope and Lifetime of Variables

Scope determines where a variable can be accessed, while lifetime refers to how long it exists.

**Types of Scope:**

1. **Local Scope:** Variables declared inside a function are accessible only within that function.

   ```python
   def test():
       x = 10  # Local variable
       print(x)

   test()
   # print(x)  # Error: x is not defined
   ```
2. **Global Scope:** Variables declared outside all functions are accessible everywhere.

   ```python
   x = 10  # Global variable
   def test():
       print(x)

   test()
   print(x)
   ```
3. **Global Keyword:** Used to modify a global variable inside a function.

   ```python
   x = 10
   def modify():
       global x
       x = 20

   modify()
   print(x)  # Output: 20
   ```

**Lifetime:**

* Local variables are destroyed when the function execution ends.
* Global variables persist throughout the program’s execution.

#### Lambda Functions

Lambda functions are anonymous, single-line functions defined using the `lambda` keyword. They are useful for short, simple operations.

**Syntax:**

```python
lambda arguments: expression
```

**Examples:**

```python
# Single-argument lambda
square = lambda x: x ** 2
print(square(5))  # Output: 25

# Multi-argument lambda
add = lambda a, b: a + b
print(add(3, 7))  # Output: 10

# Sorting with lambda
names = ["Alice", "Bob", "Charlie"]
names.sort(key=lambda name: len(name))
print(names)  # Output: ['Bob', 'Alice', 'Charlie']
```

**Key Points:**

* Lambda functions can have multiple arguments but only one expression.
* They are often used with higher-order functions like `map()`, `filter()`, and `reduce()`.

#### Higher-Order Functions

Functions that accept other functions as arguments or return them as results are called higher-order functions.

**Examples:**

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

# Using filter()
numbers = [1, 2, 3, 4]
evens = filter(lambda x: x % 2 == 0, numbers)
print(list(evens))  # Output: [2, 4]

# Using reduce()
from functools import reduce
numbers = [1, 2, 3, 4]
product = reduce(lambda x, y: x * y, numbers)
print(product)  # Output: 24
```

#### Best Practices

1. Use meaningful names for functions and parameters.
2. Keep functions short and focused on a single task.
3. Document functions with docstrings.
4. Avoid using global variables; prefer passing arguments to functions.
5. Test functions independently.

#### Exercises

**Exercise 1:**

Write a function that calculates the greatest common divisor (GCD) of two numbers.

**Solution:**

```python
def gcd(a, b):
    while b:
        a, b = b, a % b
    return a

print(gcd(48, 18))  # Output: 6
```

**Exercise 2:**

Write a function to generate the Fibonacci sequence up to a given number `n`.

**Solution:**

```python
def fibonacci(n):
    sequence = []
    a, b = 0, 1
    while a < n:
        sequence.append(a)
        a, b = b, a + b
    return sequence

print(fibonacci(10))  # Output: [0, 1, 1, 2, 3, 5, 8]
```

**Exercise 3:**

Write a function that accepts another function as an argument and applies it to a list of numbers.

**Solution:**

```python
def apply_function(func, numbers):
    return [func(num) for num in numbers]

print(apply_function(lambda x: x ** 2, [1, 2, 3, 4]))  # Output: [1, 4, 9, 16]
```

In the next chapter, we will explore Python’s data structures, including lists, tuples, dictionaries, and sets.
