Skip to main content

04.1 - Defining & Calling Functions

Theory 25 min Beginner

What is a Function?

A function is a reusable, named block of code that:

  • Takes inputs (parameters)
  • Performs an operation
  • Returns an output (return value)
# Anatomy of a function
def greet(name: str, greeting: str = "Hello") -> str:
"""Return a greeting message.

Args:
name: The person's name.
greeting: The greeting word (default: Hello).

Returns:
A formatted greeting string.
"""
return f"{greeting}, {name}!"

# Call the function
message = greet("Alice") # "Hello, Alice!"
message = greet("Bob", "Hi") # "Hi, Bob!"
def  greet  (name, greeting):
│ │ └── parameters (inputs)
│ └── function name
└── keyword

Return Values

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

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

lo, hi = min_max([3, 1, 4, 1, 5, 9]) # unpack
print(lo, hi) # 1 9

# No return (returns None implicitly)
def print_header(title):
print("=" * 40)
print(title.center(40))
print("=" * 40)

result = print_header("Report")
print(result) # None

# Early return
def find_first_negative(numbers):
for n in numbers:
if n < 0:
return n # exit immediately
return None # not found

Default Parameters

def connect(host, port=5432, timeout=30, ssl=True):
print(f"Connecting to {host}:{port} (ssl={ssl}, timeout={timeout}s)")

connect("localhost") # uses all defaults
connect("prod.server.com", port=443) # override port
connect("db.server.com", 3306, ssl=False) # positional then keyword

⚠️ Mutable default argument trap — one of the most common Python bugs:

# ❌ WRONG — the list is created ONCE and shared
def add_item(item, lst=[]):
lst.append(item)
return lst

add_item("a") # ['a']
add_item("b") # ['a', 'b'] — bug! shared list!

# ✅ CORRECT — use None as sentinel
def add_item(item, lst=None):
if lst is None:
lst = []
lst.append(item)
return lst

Keyword Arguments

def create_user(name, age, email, role="user", active=True):
return {"name": name, "age": age, "email": email,
"role": role, "active": active}

# Using keyword arguments (order doesn't matter)
user = create_user(
name="Alice",
age=30,
email="alice@example.com",
role="admin"
)

Type Hints (Python 3.5+)

Type hints document expected types — they don't enforce them at runtime but enable IDE checking.

def add(a: int, b: int) -> int:
return a + b

def process(items: list[str], limit: int = 10) -> list[str]:
return items[:limit]

# Optional (can be None)
from typing import Optional
def find_user(user_id: int) -> Optional[dict]:
...

# Union (multiple possible types)
from typing import Union
def format_value(value: Union[int, float, str]) -> str:
return str(value)

# Python 3.10+ shorthand
def find_user(user_id: int) -> dict | None:
...

Docstrings

Every function should have a docstring explaining what it does:

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

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

Returns:
The BMI value (weight / height²).

Raises:
ValueError: If height or weight is not positive.

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

Scope: LEGB Rule

Python resolves variable names using the LEGB rule:

L — Local      (inside the function)
E — Enclosing (in outer function, for nested functions)
G — Global (module level)
B — Built-in (Python built-ins: print, len, range...)
x = "global"

def outer():
x = "enclosing"

def inner():
x = "local"
print(x) # "local" — L wins

inner()
print(x) # "enclosing"

outer()
print(x) # "global"

global and nonlocal

counter = 0

def increment():
global counter # modify global variable
counter += 1

# nonlocal — modify enclosing scope
def make_counter():
count = 0
def increment():
nonlocal count
count += 1
return count
return increment

Functions are First-Class Objects

In Python, functions are objects — they can be stored in variables, passed as arguments, and returned from functions.

def double(x):
return x * 2

# Store in variable
operation = double
operation(5) # 10

# Pass as argument
numbers = [1, 2, 3, 4, 5]
doubled = list(map(double, numbers)) # [2, 4, 6, 8, 10]

# Store in data structures
operations = {
"double": double,
"triple": lambda x: x * 3
}
operations["double"](5) # 10

Key Vocabulary

TermDefinition
ParameterVariable in the function definition: def f(param)
ArgumentValue passed when calling a function: f(value)
Return valueThe result the function produces via return
Default parameterParameter with a preset value: def f(x=10)
Keyword argumentArgument specified by name: f(x=5)
Type hintOptional annotation for parameter/return types
DocstringString literal as first statement of a function
LEGB ruleScope lookup order: Local → Enclosing → Global → Built-in

Summary

  • Functions are defined with def, parameters in parentheses, and indented body
  • return sends a value back; without it, functions return None
  • Default parameters allow optional arguments — never use mutable defaults
  • Keyword arguments f(param=value) improve call-site readability
  • Type hints name: str document expected types for tools and developers
  • Docstrings explain purpose, arguments, return value, and examples
  • Functions are first-class objects — assignable, passable, and returnable