Return values and pure functions
Build predictable transformations with complete return paths and clear boundaries around side effects.
By the end, you’ll be able to…
- Return values on every intended path and use early returns appropriately.
- Return several related results as a tuple.
- Separate predictable calculations from input, output, and mutation.
01Returning creates a usable result
A function call is an expression whose value is the returned object. The caller can combine it with another calculation, compare it in a test, or pass it to another function. Printing does not provide that value. A function that prints an answer and then reaches its end returns None, which cannot serve as the numeric result the caller may expect.
Choose a contract that is consistent across all ordinary paths. If a function returns a label for positive scores but falls through for zero, the caller receives different kinds of results depending on the input. That may be intentional, but it should never happen by accident. Trace every branch and ask, 'What value reaches the caller here?' The empty collection and zero boundary are especially useful probes.
02Early returns can clarify a boundary case
An executed return ends only the current function call. It can handle a simple boundary case before the main calculation, reducing the indentation needed for the ordinary case. In an average calculation, an empty input would otherwise cause division by zero. This version explicitly returns None for no observations, and its docstring explains that choice.
None and zero communicate different outcomes: no average was available versus an average that happened to equal zero. The caller checks is None before formatting a number. A later course lesson will show contracts that reject unsuitable inputs with exceptions; for now, deliberately returning an absence marker is enough. Do not quietly choose zero just to avoid an error if that would misrepresent the meaning of the data.
03Return related results as one structured value
Python returns one object per call, but that object can be a tuple containing several related values. return count, total creates such a tuple, and the caller can unpack it into two names. Document the order and meaning so the interface does not become a guessing game. If a result grows many fields, a dictionary with named keys may be more readable.
Avoid returning several loosely related facts merely because they were convenient to calculate together. A useful result has a coherent purpose, such as the count and total of valid study sessions. The caller should be able to explain what each part means. In this example, zero and negative durations are excluded from both components, keeping their interpretation aligned. A valid empty result is the pair zero, zero.
04Keep the calculation predictable
A pure function produces its result from its arguments without observable side effects. For the same input values it gives the same result, and it does not modify input objects, print messages, read interactive input, or depend on changing outside state. This is a useful design target for calculation and transformation code, even though real programs also need effects such as displaying results and saving files.
Put those effects at a clear outer boundary: collect input, call a calculation, then present the result. A predictable function is easy to check with a small example because the check does not need a terminal conversation or hidden setup. sorted returns a new list, making it helpful for a nonmutating transformation. Purity is about behavior, not a magic keyword; calling a mutating operation inside a function still mutates.
The ideas worth keeping.
- Every intended branch should produce the result promised by the contract.
- Early returns and structured results make boundary cases and related outputs explicit.
- Predictable calculations are easier to check when input, output, and mutation stay at clear boundaries.
Make it yours.
Type and test your code right here in the interactive workspace. Run your solution, check the output, and compare with the worked guide when ready.
Return a safe largest value
Define largest_or_none(values) that returns None for an empty list and otherwise returns the largest value with max. Demonstrate an empty list and [-5, -2, -8]. Do not print inside the function.
Explore a worked solution
def largest_or_none(values): if not values: return None return max(values)print(largest_or_none([]))print(largest_or_none([-5, -2, -8]))The results are None and -2. An early return handles the empty input; returning zero instead would be misleading for a list containing only negative values.
A nonmutating positive summary
Define positive_summary(values) that returns a tuple containing a new list of positive values and their total. Demonstrate [3, -1, 0, 5], unpack both results, and print the unchanged original as well.
Explore a worked solution
def positive_summary(values): positives = [value for value in values if value > 0] return positives, sum(positives)original = [3, -1, 0, 5]selected, total = positive_summary(original)print(selected)print(total)print(original)The result contains [3, 5] and 8. Constructing a new list avoids mutating the input, while returning the related list and total keeps presentation outside the function.
What stayed with you?
Check the ideas while they’re fresh. Your answers stay in this page and feedback appears as you go.
Return a safe largest value
Define largest_or_none(values) that returns None for an empty list and otherwise returns the largest value with max. Demonstrate an empty list and [-5, -2, -8]. Do not print inside the function.
largest_or_none([]) largest_or_none([-5 -2 -8])
Keep exploring.
The official references behind this chapter, when you want a second angle or the full details.
One chapter closer.
Reading is a start. Trying it yourself makes the connection.