Skip to main content
Advertisement

5.1 Function Definitions and Returns — def, return, docstring

A function is a reusable block of code that lets you break complex programs into small, clear units. In Python, functions are first-class objects — they can be assigned to variables and passed as arguments.


def Keyword and Basic Syntax

# Basic function definition
def greet(name: str) -> str:
"""Returns a greeting message for the user."""
return f"Hello, {name}!"


message = greet("Alice")
print(message) # Hello, Alice!

# Function with no parameters
def get_separator() -> str:
return "-" * 40

print(get_separator()) # ----------------------------------------

# The function object itself (not called)
print(greet) # <function greet at 0x...>
print(type(greet)) # <class 'function'>

return Statement: Various Return Modes

# Single value return
def square(n: int | float) -> float:
return n ** 2

print(square(5)) # 25
print(square(2.5)) # 6.25


# Multiple return values (actually a tuple)
def min_max(numbers: list[int | float]) -> tuple[float, float]:
return min(numbers), max(numbers)

lo, hi = min_max([3, 1, 4, 1, 5, 9, 2, 6])
print(f"Min: {lo}, Max: {hi}") # Min: 1, Max: 9

# Can also receive as a tuple
result = min_max([10, 20, 30])
print(result) # (10, 30)
print(type(result)) # <class 'tuple'>


# Implicit None return — no return, or bare return
def print_hello():
print("Hello!")
# Implicitly returns None

def early_exit(value: int):
if value < 0:
return # Returns None and exits the function
print(f"Positive: {value}")

result1 = print_hello()
result2 = early_exit(-1)
print(result1, result2) # None None


# Early return pattern
def safe_divide(a: float, b: float) -> float | None:
if b == 0:
return None
return a / b

print(safe_divide(10, 2)) # 5.0
print(safe_divide(10, 0)) # None

Writing Docstrings

Docstrings are documentation strings written as the first statement of a function, class, or module. They are used by help() and IDE autocompletion.

# Single-line docstring — suitable for simple functions
def add(a: int, b: int) -> int:
"""Returns the sum of two integers."""
return a + b


# Multi-line docstring — for complex functions
def process_data(data: list[dict], threshold: float = 0.5) -> list[dict]:
"""
Processes a list of data and returns only items above the threshold.

Parameters
----------
data : list[dict]
List of data to process. Each item must contain a 'value' key.
threshold : float, optional
Filtering threshold (default: 0.5)

Returns
-------
list[dict]
A new list containing only items with value >= threshold

Raises
------
ValueError
When data is empty

Examples
--------
>>> process_data([{"value": 0.3}, {"value": 0.7}], threshold=0.5)
[{'value': 0.7}]
"""
if not data:
raise ValueError("Data is empty")
return [item for item in data if item.get("value", 0) >= threshold]


print(process_data.__doc__)
help(add)

Google Style Docstring

def calculate_bmi(weight_kg: float, height_m: float) -> float:
"""Calculate BMI (Body Mass Index).

Args:
weight_kg: Weight in kilograms
height_m: Height in meters

Returns:
BMI value (weight / height²)

Raises:
ValueError: If weight or height is non-positive

Example:
>>> calculate_bmi(70, 1.75)
22.86
"""
if weight_kg <= 0 or height_m <= 0:
raise ValueError("Weight and height must be positive")
return round(weight_kg / (height_m ** 2), 2)


print(calculate_bmi(70, 1.75)) # 22.86

Type Hints and Function Signatures

from typing import Optional, Union


# Basic type hints
def greet_user(name: str, age: int) -> str:
return f"{name} (age {age})"


# Optional: can be None
def find_user(user_id: int) -> Optional[dict]:
db = {1: {"name": "Alice"}, 2: {"name": "Bob"}}
return db.get(user_id)


# Union: accepts multiple types (Python 3.10+: use X | Y)
def stringify(value: Union[int, float, str]) -> str:
return str(value)

def stringify_new(value: int | float | str) -> str:
return str(value)


# Collection type hints
from typing import Any

def process_items(
items: list[str],
mapping: dict[str, Any],
flags: tuple[bool, ...]
) -> list[str]:
return [item for item in items if item in mapping]


# Callable type hints
from typing import Callable

def apply_twice(func: Callable[[int], int], value: int) -> int:
"""Apply a function twice"""
return func(func(value))

print(apply_twice(lambda x: x * 2, 3)) # 12


# Complex return type
from typing import TypeAlias

Matrix: TypeAlias = list[list[float]]

def identity_matrix(n: int) -> Matrix:
"""Generate an n×n identity matrix"""
return [[1.0 if i == j else 0.0 for j in range(n)] for i in range(n)]

for row in identity_matrix(3):
print(row)
# [1.0, 0.0, 0.0]
# [0.0, 1.0, 0.0]
# [0.0, 0.0, 1.0]

Nested Functions

# Inner function — only valid inside the outer function
def make_greeting(language: str):
"""Generate a greeting function by language"""

def korean_greeting(name: str) -> str:
return f"안녕하세요, {name}님!"

def english_greeting(name: str) -> str:
return f"Hello, {name}!"

def japanese_greeting(name: str) -> str:
return f"こんにちは、{name}さん!"

greetings = {
"ko": korean_greeting,
"en": english_greeting,
"ja": japanese_greeting,
}
return greetings.get(language, english_greeting)


greet_ko = make_greeting("ko")
greet_en = make_greeting("en")

print(greet_ko("Kim Cheolsu")) # 안녕하세요, Kim Cheolsu님!
print(greet_en("Alice")) # Hello, Alice!


# Separate complexity with inner functions
def validate_and_process(data: list[int]) -> dict:
def validate(items: list[int]) -> list[str]:
errors = []
if not items:
errors.append("List is empty")
if any(x < 0 for x in items):
errors.append("Contains negative values")
if len(items) > 1000:
errors.append("Exceeds maximum of 1000 items")
return errors

def compute_stats(items: list[int]) -> dict:
return {
"count": len(items),
"sum": sum(items),
"mean": sum(items) / len(items),
"min": min(items),
"max": max(items),
}

errors = validate(data)
if errors:
return {"success": False, "errors": errors}

return {"success": True, "stats": compute_stats(data)}


result = validate_and_process([3, 7, 2, 9, 1])
print(result)

Real-world Example: Good Function Design

# Single Responsibility Principle

# Bad: one function doing too many things
def process_user_data_bad(user_data: dict) -> None:
if not user_data.get("name"):
raise ValueError("No name")
if not user_data.get("email"):
raise ValueError("No email")
if "@" not in user_data.get("email", ""):
raise ValueError("Invalid email format")
print(f"DB save: {user_data}")
print(f"Email sent to: {user_data['email']}")
print(f"Log: User {user_data['name']} registered")


# Good: each function has one responsibility
def validate_user(user_data: dict) -> list[str]:
"""Validate user data. Returns list of errors."""
errors = []
if not user_data.get("name"):
errors.append("Name is required")
if not user_data.get("email"):
errors.append("Email is required")
elif "@" not in user_data["email"]:
errors.append("Email format is invalid")
return errors


def save_user(user_data: dict) -> int:
"""Save user to DB. Returns created ID."""
print(f"DB save: {user_data['name']}")
return 42 # Simulation


def send_welcome_email(email: str, name: str) -> bool:
"""Send welcome email. Returns success status."""
print(f"Welcome email sent: {name} <{email}>")
return True


def register_user(user_data: dict) -> dict:
"""Orchestrate the user registration flow."""
errors = validate_user(user_data)
if errors:
return {"success": False, "errors": errors}

user_id = save_user(user_data)
send_welcome_email(user_data["email"], user_data["name"])

return {"success": True, "user_id": user_id}


result = register_user({"name": "Alice", "email": "alice@example.com"})
print(result)

Pro Tips

1. Dynamic Access to Function Annotations

def example(x: int, y: float = 1.0) -> str:
"""Example function"""
return str(x + y)

print(example.__annotations__)
# {'x': <class 'int'>, 'y': <class 'float'>, 'return': <class 'str'>}

print(example.__defaults__) # (1.0,)
print(example.__name__) # example

2. Functions Returning Functions (Factory)

def multiplier(factor: int):
"""Returns a function that multiplies by factor"""
def multiply(x: int) -> int:
return x * factor
return multiply

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

print(double(5)) # 10
print(triple(5)) # 15

3. Prefer Pure Functions

# Pure function: same input → same output, no side effects
# Easier to test, fewer bugs

# Impure: depends on global state
count = 0
def increment_bad():
global count
count += 1
return count

# Pure: output determined only by input
def increment(current: int) -> int:
return current + 1

# Date dependency (impure → improved to pure)
from datetime import date

def get_age_bad(birth_year: int) -> int:
return date.today().year - birth_year # Result varies by run date

def get_age(birth_year: int, current_year: int) -> int:
return current_year - birth_year # Easy to test

print(get_age(1990, 2024)) # 34
Advertisement