Course outline

Python / Foundations / Lesson 4

Functions, contracts, and scope

Turn repeated calculations into small functions whose inputs, results, and failure conditions are explicit.

About 4 min readContent reviewed
On this page

Lesson goals

  • Define a useful contract
  • Design calls for readers

Define a useful contract

A function groups a coherent operation behind a name. Its parameters describe required inputs, and return provides a result to the caller. A useful contract states meaning and units as well as accepted types: a duration in minutes and a duration in seconds can both be integers but are not interchangeable. Describe exceptional cases such as a negative quantity before implementing them. A docstring is a string placed first in the function body to document that contract. A short function with a precise purpose is easier to test than one that mixes calculation, user interaction, and persistent storage.

Arguments and return values

A parameter is a name in the definition; an argument is a value supplied during a call. Positional arguments are matched by order, while keyword arguments make the chosen parameter explicit. Default values support common cases but should not hide surprising policy. A keyword-only parameter after * is useful when a Boolean option would otherwise be unclear at the call site. return immediately ends that function call. Falling off the end, or using return without an expression, returns None. Printing a value has a different effect: it displays text but does not provide that displayed value as the function’s result.

Keep calculation independent

Pure calculation accepts values and returns a value without changing external state. That makes the result easy to predict, compose, and test. The example computes a subtotal in integer cents; the keyword-only shipping argument prevents confusion between quantity and shipping. Every input needed for the calculation appears in the signature. The caller controls whether to print, store, or further transform the result. This separation also avoids repeated prompts inside a reusable function. Not every useful function can be pure, but keeping the calculation core independent of file and terminal operations limits how much behavior must be simulated during testing.

python Keep calculation independent — worked example
def total_cents(price, quantity, *, shipping=0):
    """Return the total in cents for a nonnegative order."""
    return price * quantity + shipping

print(total_cents(250, 3, shipping=100))
Expected output
850

Three items cost 750 cents and shipping adds 100. The function returns 850; only the caller prints it.

Names and mutable arguments

Function execution creates a local namespace. Assigning a local parameter name does not rebind the caller’s name, but mutating an object accessible through that parameter can change the object the caller sees. Default argument objects are created when the def statement executes, rather than freshly for every call. A mutable list default can therefore retain data across calls unintentionally. Use None as a sentinel and create a list inside the body when independence is required. The example demonstrates that two ordinary calls receive independent result lists, while a caller can explicitly provide an existing list to extend.

python Names and mutable arguments — worked example
def append_label(label, labels=None):
    if labels is None:
        labels = []
    labels.append(label)
    return labels

print(append_label("red"))
print(append_label("blue"))
Expected output
['red']
['blue']

Each omitted labels argument creates a fresh list inside the function, so the second result does not contain red.

Design calls for readers

Prefer explicit parameters over reading configuration from distant globals. Small signatures reveal dependencies, while a function with many unrelated arguments often needs a clearer responsibility. Name functions for what they do or compute, and keep callers at a consistent level of detail. When debugging, trace one call with concrete arguments and record the returned object separately from printed output. Test independent repeated calls because one successful call cannot reveal a shared mutable default. Also test a supplied mutable object when mutation is part of the contract. Documentation should say whether a function changes inputs, returns a new object, or both.

Common errors and fixes

The caller receives None instead of the displayed total

Why it happens
The function prints its calculation but never returns it.
How to fix it
Return the computed value and print at the interaction boundary.
Prevent it next time
Test the returned result directly rather than relying on terminal output.

A later call unexpectedly contains earlier items

Why it happens
Calls share the same mutable default object.
How to fix it
Use a None sentinel and initialize inside the body.
Prevent it next time
Include two independent consecutive calls in the test cases.

A helper unexpectedly changes its input list

Why it happens
The parameter refers to the same list object as the caller.
How to fix it
Copy before mutation or explicitly document intentional mutation.
Prevent it next time
Specify ownership and mutation behavior in the contract.

Check your understanding

1. Design a function for converting integer minutes into a pair of hours and minutes.

Show worked solution

Define split_minutes(total) to return (total // 60, total % 60), with a documented nonnegative-input contract. Calling split_minutes(137) returns (2, 17). Returning the pair keeps formatting separate and lets another function verify hours * 60 + minutes == total.

2. Explain the difference between assigning items = [] inside a function and calling items.clear().

Show worked solution

Assignment changes the local binding to a new list, leaving the original caller-owned list intact. clear mutates the existing object, so every name referring to that list observes it becoming empty. Choose the operation according to the documented ownership contract.

Key takeaways

  • Return values enable composition.
  • A binding change and an object mutation are different actions.
  • Defaults and side effects belong in the function contract.

Official references