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
@app.get("/")), which registers it as a route handler. The decorator pattern only works because Python functions are first-class objects.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.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) |