A function gives a name to a reusable operation. Good functions make the caller's intent visible: minutes_to_label(75) says more than a repeated block of arithmetic. After values and control flow, you can use functions to separate calculations from printing and later test them without running an entire application.
Inputs, outputs, and a clear contract
A parameter is a name in a function definition. An argument is the value passed by a caller. return sends a result back and ends that call. Printing writes text to the terminal; it does not provide a result for another calculation. A function that reaches its end without returning a value returns None.
def minutes_to_label(minutes, *, unit="min"):
"""Format a non-negative whole number of minutes."""
if type(minutes) is not int:
raise TypeError("minutes must be a whole number")
if minutes < 0:
raise ValueError("minutes must not be negative")
return f"{minutes} {unit}"
label = minutes_to_label(25)
print(label)
print(minutes_to_label(40, unit="minutes"))
The output is 25 min and 40 minutes. This function's contract is its promise about accepted inputs and returned results. It deliberately rejects booleans as minutes: Python treats bool as a subclass of int, so an ordinary integer-instance check would accept True. Exact type checks are useful for this particular boundary, not a rule to apply to every function.
The bare * makes unit keyword-only. Callers must write unit="minutes", which makes the optional choice readable. Ordinary parameters can be passed positionally or by name. Defaults fill in omitted arguments; they do not override an explicitly supplied value. Python also supports / for positional-only parameters, *args for extra positional arguments, and **kwargs for extra keyword arguments. Start with an explicit signature and add flexibility only when callers need it.
The function-definition tutorial explains these calling forms. The triple-quoted string directly beneath a definition is its docstring, documentation accessible through help(minutes_to_label).
Defaults can accidentally share state
Default arguments are evaluated when the function is defined. A list used as a default would be reused across calls. Instead, create the list inside the call:
def add_topic(topic, topics=None):
if topics is None:
topics = []
topics.append(topic)
return topics
first = add_topic("loops")
second = add_topic("functions")
print(first)
print(second)
existing = ["setup"]
add_topic("types", existing)
print(existing)
The output lists are ['loops'], ['functions'], and ['setup', 'types']. Passing existing shares that list with the function. Appending changes the shared object, so the caller sees the update. If the contract should preserve the input, begin with topics = list(topics) after handling None, then return the new list.
Understand where names live
Scope determines where a name can be used. A function normally finds names in its local scope, any enclosing function scopes, the module, and built-ins. An assignment inside a function normally creates a local name; it does not replace a same-named variable in the caller. See the scope tutorial.
Prefer passing needed data as arguments and returning results. A calculation that reads and modifies module-level variables is harder to reuse because the caller must understand hidden state. global and nonlocal exist for specific needs, but most beginner functions become clearer without them. A nested function can remember enclosing values; that behavior is called a closure.
Functions are values too. You can pass len as the key to sorted, for example. A short lambda can supply a simple expression, but a named function is better when the operation needs validation, explanation, or several steps.
Pitfalls and practice
Avoid naming variables list, sum, or str; those names hide useful built-ins. Keep validation and return behavior consistent across branches. A missing return in one branch can leave a caller unexpectedly holding None.
Exercise: write total_minutes(sessions, *, minimum=0) that sums values meeting the minimum without changing the supplied list. Try [10, 25, 40] with minimum 20 and with an empty list.
Solution guidance: start a local total at zero, loop through sessions, add qualifying values, and return the total after the loop. Expect 65 and 0. Printing inside the function should be unnecessary. Finally, verify that the original list still has its original three values.

