Skip to main content

Writing Small Functions

What You'll Learn

How to write functions that do one thing well — with clear parameters, return values, and sensible defaults.

Why Functions?

Functions let you:

  • Avoid repetition — write code once, call it many times
  • Test logic independently — test each piece separately
  • Name a conceptcalculate_tax() is clearer than 3 lines of math
  • Isolate changes — fix one function without touching everything else

Defining and Calling Functions

# Define
def greet(name):
return f"Hello, {name}!"

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

Parameters and Arguments

# Positional parameters
def add(a, b):
return a + b

add(3, 5) # a=3, b=5 → 8

# Default parameters (must come after required ones)
def greet(name, greeting="Hello"):
return f"{greeting}, {name}!"

greet("Alice") # "Hello, Alice!"
greet("Bob", "Hi") # "Hi, Bob!"
greet("Carol", greeting="Hey") # "Hey, Carol!"

# Keyword arguments (order doesn't matter)
def create_user(name, age, email):
return {"name": name, "age": age, "email": email}

create_user(age=30, email="a@b.com", name="Alice")

Return Values

# Return a single value
def square(n):
return n ** 2

# Return multiple values (actually returns a tuple)
def min_max(numbers):
return min(numbers), max(numbers)

low, high = min_max([3, 1, 4, 1, 5])
print(f"min={low}, max={high}") # min=1, max=5

# Return early when condition not met
def safe_divide(a, b):
if b == 0:
return None # early return — caller must handle None
return a / b

result = safe_divide(10, 0)
if result is None:
print("Division by zero")

*args and **kwargs

Collect a variable number of arguments:

# *args — any number of positional arguments
def total(*numbers):
return sum(numbers)

total(1, 2, 3) # 6
total(10, 20, 30, 40) # 100

# **kwargs — any number of keyword arguments
def display(**info):
for key, value in info.items():
print(f"{key}: {value}")

display(name="Alice", age=30, city="London")

# Combine them
def log(level, *messages, prefix=">>"):
for msg in messages:
print(f"{prefix} [{level}] {msg}")

log("INFO", "Starting", "Connecting", prefix="---")

Type Hints

Type hints document what types a function expects and returns:

def calculate_discount(price: float, rate: float) -> float:
"""Apply discount rate to price and return discounted amount."""
return price * (1 - rate)

def get_username(user_id: int) -> str | None:
"""Return username or None if not found."""
...

Type hints are not enforced at runtime but:

  • Act as documentation
  • Enable IDE auto-complete and error checking
  • Let tools like mypy find type bugs

One Function, One Job (SRP)

The Single Responsibility Principle: each function should do one thing.

# ❌ Too many responsibilities
def process_user(name, age, data_file):
# validates, loads file, processes, saves — too much!
if not name:
raise ValueError("name required")
with open(data_file) as f:
data = json.load(f)
data["user"] = name
with open(data_file, "w") as f:
json.dump(data, f)
return data

# ✅ Split into focused functions
def validate_name(name: str) -> None:
if not name:
raise ValueError("name required")

def load_data(path: str) -> dict:
with open(path, encoding="utf-8") as f:
return json.load(f)

def save_data(path: str, data: dict) -> None:
with open(path, "w", encoding="utf-8") as f:
json.dump(data, f, indent=2)

def process_user(name: str, data_file: str) -> dict:
validate_name(name)
data = load_data(data_file)
data["user"] = name
save_data(data_file, data)
return data

Pure Functions

A pure function has no side effects — given the same input, it always returns the same output:

# ✅ Pure — easy to test
def calculate_tax(amount: float, rate: float) -> float:
return amount * rate

# ❌ Impure — reads from external state
def calculate_tax(amount: float) -> float:
rate = get_tax_rate_from_database() # depends on external state
return amount * rate

Prefer pure functions where possible. They're predictable, testable, and safe to call repeatedly.

Lambda Functions

Small, anonymous one-line functions:

# Regular function
def double(x):
return x * 2

# Lambda equivalent
double = lambda x: x * 2

# Most useful as an argument to sorted(), map(), filter()
names = ["Charlie", "Alice", "Bob"]
sorted_names = sorted(names, key=lambda n: n.lower())

data = [{"name": "Alice", "age": 30}, {"name": "Bob", "age": 25}]
sorted_data = sorted(data, key=lambda d: d["age"])

Common Mistakes

MistakeFix
Mutable default argumentUse None default, create inside function
Function does too many thingsSplit into smaller functions
Missing return (returns None implicitly)Make return explicit
Very long functions (>20-30 lines)Extract helpers
Generic names: process(), do_thing()Use descriptive names

Quick Reference

# Basic function
def func(param):
return value

# With defaults
def func(x, y=10):
...

# *args and **kwargs
def func(*args, **kwargs):
...

# Type hints
def func(name: str, count: int = 1) -> list[str]:
...

# Lambda
key_fn = lambda x: x["field"]

# Return multiple values
def fn():
return a, b # returns (a, b) tuple

x, y = fn() # unpack

What's Next

Lesson 2: Modules, Imports, and Entrypoints