Before this lesson
Understand lists, loops and variables, and evaluate simple expressions.
What you will be able to do
Separate calculations from output and avoid shared mutable defaults.
Understand the mechanism
A function body runs when called. Parameters are inputs and return provides output; paths without a return produce None. Pure formatting or calculation functions are easier to verify than duplicated output expressions.
Keyword arguments clarify calls. Default values are evaluated at definition time, so a default list can be shared across calls. Use a None sentinel and allocate a fresh container inside when needed.
Prefer returning results over hidden global mutation. Document intentional mutation of arguments. Type annotations assist readers and tools but do not automatically validate runtime values.
Test requirements such as zero duration and negative input. Assertions are useful for exercise checks, not as the only production input validation because optimized execution can omit them.
Runnable example
Use Python 3.11 or later. Save lesson.py in a separate practice directory and run python3 lesson.py, or py lesson.py on Windows. Examples use only the standard library.
def duration_label(minutes):
if type(minutes) is not int or minutes < 0:
raise ValueError("minutes must be a nonnegative integer")
hours, rest = divmod(minutes, 60)
return f"{hours}h {rest}m"
def collect(title, items=None):
if items is None:
items = []
items.append(title)
return items
assert duration_label(125) == "2h 5m"
assert duration_label(0) == "0h 0m"
print(duration_label(125))
print(collect("Python"), collect("SQL"))
Trace and verify
Expect 2h 5m and separate one-item lists. Temporarily use a default list to observe sharing, then restore. The strict integer check deliberately rejects booleans despite their integer subclass relationship.
Common mistakes and boundaries
Do not hide unclear contracts behind broad exception handling. This example deliberately mutates a list supplied by the caller; document ownership.
Practice challenge
Add average calculation with None for empty data, no source mutation and an explicit numeric policy.
Reference solution and reasoning
Validate at the boundary and return numeric results so callers choose presentation precision independently.
Lab: define a function contract before averaging
An average needs a contract: units, empty input and invalid values. This function accepts nonnegative integer minutes, returns None when no average exists, and raises for invalid observations. Zero then means a real measured value rather than also meaning missing or corrupt data.
def average_minutes(values):
if any(type(value) is not int or value < 0 for value in values):
raise ValueError("minutes must be nonnegative integers")
if not values:
return None
return sum(values) / len(values)
print(average_minutes([]))
print(average_minutes([0, 30, 60]))
for bad in [[-1], [True], ["30"]]:
try:
average_minutes(bad)
except ValueError:
print("rejected")
assert average_minutes([0]) == 0
Expected observations
Output is None, 30.0 and three rejected lines. True is rejected even though bool is a subclass of int, because a Boolean is not a duration observation in this contract.
Why this happens and where it stops applying
The exact-type check is a deliberate domain restriction, not a universal typing rule. This implementation expects a list-like, repeatable input with len; a generator would be consumed by validation and lacks len. For streaming input, use a single-pass design or materialize it with a size bound. Keep calculation separate from presentation so the UI can render None as No records without complicating the pure function.
Extension challenge
If fractional minutes become valid, why not simply remove type validation?
Reasoned solution
Strings, Booleans, NaN and infinity could enter the calculation. Redefine accepted numeric types and finiteness, retain the nonnegative constraint, and test empty input, zero, fractions and NaN. The unit must remain consistent.