Functions
Design reusable functions with clear inputs, outputs, scope, documentation, and errors.
A function gives a behavior a name and defines an interface between callers and an implementation. Good functions reduce repetition, isolate decisions, and make programs easier to test.
Define and call a function
def calculate_area(width, height):
area = width * height
return area
room_area = calculate_area(4.5, 3)
print(room_area) # 13.5width and height are parameters. 4.5 and 3 are arguments. A call evaluates arguments,
binds them to parameters, executes the body, and produces the returned value.
If execution reaches the end without return, the function returns None.
Return values, do not only print
def format_total(amount):
return f"${amount:,.2f}"
label = format_total(1250)
print(label)Returning separates computation from presentation. A caller can print, log, store, or test the
result. Use print inside a function only when producing output is genuinely its responsibility.
A function can return multiple values as one tuple:
def minimum_and_maximum(values):
return min(values), max(values)
low, high = minimum_and_maximum([8, 3, 11])Reusable design
A focused function usually:
- does one coherent job;
- has a verb-based name that states intent;
- receives dependencies as parameters rather than relying on hidden global state;
- returns a useful value;
- handles or clearly rejects invalid input.
def percentage(part, whole):
if whole == 0:
raise ValueError("whole must not be zero")
return part / whole * 100Raise an exception when the function cannot honor its contract. Do not return an unrelated
sentinel such as -1 unless that value is explicitly part of the interface.
Type hints
Type hints document expected values and help static tools; Python does not enforce them at runtime:
def average(values: list[float]) -> float:
if not values:
raise ValueError("values must not be empty")
return sum(values) / len(values)When a function only needs iteration, a broader annotation such as Iterable[float] from
collections.abc may be more flexible. Start with concrete types while learning, then widen
interfaces when a real use case requires it.
Docstrings
Put a string literal immediately inside a public or non-obvious function:
def clamp(value: float, minimum: float, maximum: float) -> float:
"""Return value limited to the inclusive minimum-to-maximum range."""
if minimum > maximum:
raise ValueError("minimum must not exceed maximum")
return max(minimum, min(value, maximum))Document purpose, important constraints, exceptional behavior, and units. Avoid restating every line of obvious code.
Scope and name resolution
Names assigned inside a function are local:
tax_rate = 0.2
def total_with_tax(subtotal):
total = subtotal * (1 + tax_rate)
return totalThe function reads the module-level tax_rate, but total exists only during the call.
Avoid modifying globals from functions:
def add_task(tasks, task):
return [*tasks, task]Passing state explicitly makes behavior easier to understand and test. global and nonlocal
exist for specialized cases, but they are not substitutes for clear data flow.
Mutation versus new values
Be explicit about whether a function mutates an argument:
def normalized_names(names):
return [name.strip().casefold() for name in names]This version returns a new list and leaves the caller's list unchanged. Mutation can be valid for performance or an intentionally stateful API, but make it clear in the name and documentation.
Early returns
Guard clauses keep the main path shallow:
def shipping_cost(total, is_member):
if total < 0:
raise ValueError("total cannot be negative")
if is_member or total >= 50:
return 0
return 5This is often clearer than deeply nested if/else blocks.
Functions as values
Functions can be assigned, passed, and returned:
def normalize(text):
return text.strip().casefold()
names = [" Grace", "ada "]
print(sorted(names, key=normalize))Pass the function itself as key=normalize; writing normalize() would call it immediately
without the required argument.
Use a lambda for a short, local expression:
records = [("Ada", 36), ("Grace", 37)]
by_age = sorted(records, key=lambda record: record[1])Use def when logic needs a name, documentation, multiple statements, or reuse.
Common mistakes
- Forgetting
returnand receivingNone. - Printing a result inside a function when callers need to use it.
- Using mutable default arguments; the next lesson shows the safe pattern.
- Depending on or mutating global state unnecessarily.
- Confusing
returnwithbreak;returnexits the entire function. - Catching every exception and hiding programming errors.
- Writing one large function that mixes input, processing, and output.
Decision guidance
- Extract a function when logic repeats, names a meaningful operation, or deserves isolated testing.
- Pass required information in and return results out.
- Raise a specific exception for invalid contracts; handle expected exceptions at the layer that can make a useful decision.
- Prefer pure functions for transformations, while allowing explicit mutation where it models the intended API.
- Use
defby default and lambdas only for small callback expressions.
Practice
- Write
is_even(number)returning a boolean without anifstatement. - Write
safe_average(values)that rejects an empty list withValueError. - Refactor a temperature conversion script into input-free conversion functions.
- Write a documented
clampfunction and test normal, boundary, and invalid-range cases.
LEARNING RECORD
Finish this lesson
Mark it complete when you can explain the main decision without looking.
KNOWLEDGE CHECK