Defining Functions — def, Parameters and return

Functions are the fundamental unit of reusable code in Python — they package a block of logic under a name so you can call it from anywhere, pass data in, and get results back. In FastAPI, virtually everything you write is a function: route handlers, dependency functions, background tasks, event handlers, and utility helpers. Understanding how to define functions cleanly — with clear parameters, explicit return values, and meaningful names — is the single most important habit to build before moving on to the more advanced Python features that FastAPI relies on.

Defining and Calling a Function

# def keyword, function name (snake_case), parentheses, colon
def greet():
    print("Hello!")

# Call the function
greet()   # Hello!

# Function with parameters
def greet_user(name):
    print(f"Hello, {name}!")

greet_user("Alice")   # Hello, Alice!
greet_user("Bob")     # Hello, Bob!

# Function with a return value
def add(a, b):
    return a + b

result = add(3, 4)
print(result)   # 7

# Without return — function returns None implicitly
def say_hi():
    print("Hi!")

val = say_hi()
print(val)   # None
Note: Python functions are first-class objects — they can be assigned to variables, stored in lists and dicts, passed as arguments to other functions, and returned from functions. This is the foundation of Python’s functional programming features and is heavily used in FastAPI: you pass your function to the router decorator (@app.get("/")), which registers it as a route handler. The decorator pattern only works because Python functions are first-class objects.
Tip: Use the single responsibility principle for functions: each function should do one thing and do it well. A function named validate_and_save_user does two things — split it into validate_user() and save_user(). Short, focused functions are easier to test, easier to reuse, and much easier to debug. In FastAPI, route handlers that do too much are the primary cause of hard-to-maintain API code.
Warning: The return statement exits the function immediately — any code after it in the same branch is unreachable. This is intentional and useful for guard clauses: return early when a condition fails rather than deeply nesting the happy path. FastAPI route handlers use this pattern constantly: check if a resource exists, return a 404 immediately if not, then continue with the happy path logic.

Return Values

# Single return value
def square(n):
    return n ** 2

print(square(5))   # 25

# Multiple return values (returned as a tuple)
def min_max(numbers):
    return min(numbers), max(numbers)   # returns a tuple

low, high = min_max([3, 1, 4, 1, 5, 9])
print(low, high)   # 1 9

# Destructure the tuple
result = min_max([3, 1, 4, 1, 5, 9])
print(result)        # (1, 9)
print(result[0])     # 1

# Early return — guard clause pattern
def get_user_role(user_id: int):
    if user_id <= 0:
        return None          # guard: invalid ID
    user = db.get(user_id)
    if user is None:
        return None          # guard: not found
    return user.role         # happy path

# Return different types based on condition
def divide(a, b):
    if b == 0:
        return None          # signal error with None
    return a / b

result = divide(10, 0)
if result is None:
    print("Cannot divide by zero")
else:
    print(result)

Functions as First-Class Objects

# Assign a function to a variable
def greet(name):
    return f"Hello, {name}!"

say_hello = greet      # no () — assigns the function, not its result
print(say_hello("Alice"))   # Hello, Alice!

# Store functions in a dict (dispatch table pattern)
def handle_create():  return "Created"
def handle_read():    return "Read"
def handle_delete():  return "Deleted"

handlers = {
    "POST":   handle_create,
    "GET":    handle_read,
    "DELETE": handle_delete,
}

method  = "GET"
result  = handlers[method]()   # calls handle_read()
print(result)   # Read

# Pass a function as an argument
def apply(func, value):
    return func(value)

print(apply(square, 4))      # 16  (square is from above)
print(apply(str.upper, "hello"))  # HELLO

Nested Functions

# Functions defined inside other functions
def outer(x):
    def inner(y):          # inner is local to outer
        return x + y       # inner can access outer's variable x
    return inner           # return the inner function

add5 = outer(5)           # add5 is now the inner function with x=5
print(add5(3))            # 8
print(add5(10))           # 15

# This is the closure pattern — inner function captures outer scope
# FastAPI dependency injection uses this pattern extensively

Common Mistakes

Mistake 1 — Calling a function without parentheses

❌ Wrong — assigning the function object instead of calling it:

result = add    # assigns the function — no call!
print(result)   # <function add at 0x...> — not the result

✅ Correct — include parentheses to call the function:

result = add(3, 4)   # ✓ calls the function, result = 7

Mistake 2 — Modifying a mutable default argument

❌ Wrong — list default is shared across all calls:

def add_item(item, items=[]):   # DANGEROUS: list created once at definition
    items.append(item)
    return items

print(add_item("a"))   # ["a"]
print(add_item("b"))   # ["a", "b"] — unexpected! Previous call's state persists

✅ Correct — use None as default and create inside the function:

def add_item(item, items=None):
    if items is None:
        items = []       # fresh list for every call ✓
    items.append(item)
    return items

Mistake 3 — Code after return is never reached

❌ Wrong — dead code after return:

def get_price(item):
    return item.price
    discount = 0.1       # never runs!
    return item.price * (1 - discount)

✅ Correct — compute before returning:

def get_price(item, discount=0.0):
    return item.price * (1 - discount)   # ✓

Quick Reference

Pattern Code
Define function def name(params): ...
Call function name(args)
Return a value return value
Return multiple return a, b → tuple
Unpack return x, y = func()
Return nothing Omit return or return None
Guard clause if not valid: return None
Pass as argument apply(func, value)

🧠 Test Yourself

What does the following code print?
def double(n):
    n * 2

result = double(5)
print(result)