Design focused functions with parameters, return values, defaults, scope, argument unpacking, and explicit error behavior.
A function gives a meaningful name to a reusable piece of behavior. It receives information through parameters, performs one focused job, and can return a result to its caller. Well-designed functions make a program easier to read, test, reuse, and change because each rule has a clear home.
Learning goals
Define and call a function with clear parameters.
Explain the difference between an argument, a parameter, print, and return.
Use positional, keyword, default, and keyword-only parameters deliberately.
Keep local state inside a function and avoid hidden global dependencies.
Write safe defaults and avoid shared mutable default values.
Communicate contracts with type hints, docstrings, and useful exceptions.
Define a function, then call it
A function definition begins with def, followed by its name, parentheses, and a colon. The indented body does not run when Python reads the definition; it runs each time the function is called. Choose a verb-based name that states the behavior and parameters that name the information the behavior requires.
PYTHON · EDITABLE
defprogress_percent(completed, total):
percent = completed / total *100return percent
result = progress_percent(3,12)print(f"{result:.0f}% complete")
completed and total are parameters: names inside the function definition. The values 3 and 12 are arguments supplied by the caller. During the call, Python binds those arguments to the parameters, executes the body, and sends the returned value back to the call site.
Return data instead of only printing
print creates a visible side effect in the terminal and returns None. return ends the current function call and gives a value to the caller. Returning a value lets another function calculate with it, format it differently, store it, or test it without capturing terminal output.
Positional arguments are matched by order. Keyword arguments are matched by name and make calls easier to read when several values have similar types. A default makes an argument optional. Put required parameters before optional ones, and use a bare * when later parameters should be keyword-only.
The bare * does not collect values here. It marks every following parameter as keyword-only, so a call cannot accidentally swap discount_percent and delivery_cents. Defaults should represent honest, unsurprising behavior rather than hiding information the function truly needs.
Pack and unpack arguments carefully
A parameter written as *args collects extra positional arguments into a tuple. A parameter written as **kwargs collects extra keyword arguments into a dictionary. At a call site, * unpacks an iterable into positional arguments and ** unpacks a mapping into keyword arguments. These tools are useful for adapters and forwarding APIs, but explicit parameters remain clearer when a function has a known contract.
PYTHON · EDITABLE
defaverage(*values:float)->float:ifnot values:raise ValueError("at least one value is required")returnsum(values)/len(values)defdescribe_course(name:str,*, level:str, lessons:int)->str:returnf"{name}: {level}, {lessons} lessons"
scores =[80,90,100]
course_options ={"level":"beginner","lessons":10}print(average(*scores))print(describe_course("Python",**course_options))
Return and unpack related results
A function always returns one object. Writing return minimum, maximum creates a tuple, which the caller can unpack into two names. This is useful for a small, fixed group of related results. When many fields need names or the result grows over time, a dictionary, named tuple, or data class communicates the structure more clearly.
PYTHON · EDITABLE
defscore_bounds(scores:list[int])->tuple[int,int]:ifnot scores:raise ValueError("scores must not be empty")returnmin(scores),max(scores)
lowest, highest = score_bounds([84,61,93])print(f"Range: {lowest}–{highest}")
Use early returns to keep the main path visible
return can appear before the final line of a function. An early return is useful when a special case has a complete answer or when a guard rejects invalid input. After Python executes return, no later statement in that function call runs.
PYTHON · EDITABLE
defprogress_label(completed:int, total:int)->str:if total <=0:raise ValueError("total must be positive")if completed <=0:return"Not started"if completed >= total:return"Complete"returnf"{completed / total:.0%} complete"
Avoid mutable default arguments
Default expressions are evaluated once when Python executes the def statement, not once per call. A list or dictionary used as a default is therefore shared by every call that omits that argument. Use None as a sentinel, then create a new collection inside the function.
PYTHON · EDITABLE
defadd_note(note:str, notes:list[str]|None=None)->list[str]:if notes isNone:
notes =[]
notes.append(note)return notes
first = add_note("Validate input")
second = add_note("Return a value")print(first)print(second)
Keep scope and dependencies clear
Names assigned inside a function are local to that call. Python looks for a name in local, enclosing, global, and built-in scopes, but a function is easier to understand when required information arrives through parameters instead of being read from changeable global state.
PYTHON · EDITABLE
tax_percent =20# Hidden dependency: behavior changes when a global changes.deftotal_with_hidden_tax(subtotal):return subtotal + subtotal * tax_percent //100# Explicit dependency: the caller controls the rule.deftotal_with_tax(subtotal:int, tax_percent:int)->int:return subtotal + subtotal * tax_percent //100print(total_with_tax(10_000,20))
Reading a stable module-level constant can be reasonable, but changing global state from inside a function couples distant parts of a program. Prefer returning a new value. Use nonlocal or global only when the shared-state design is deliberate and clearly justified.
Protect the function contract
A function contract describes valid inputs, the returned result, and possible failures. Validate untrusted or business-critical values at the boundary. Raise a specific exception with a useful message when the caller violates the contract; do not silently invent a plausible result.
PYTHON · EDITABLE
deforder_total(unit_cents:int, quantity:int)->int:"""Return the total in cents for a positive quantity."""if unit_cents <0:raise ValueError("unit_cents must not be negative")if quantity <1:raise ValueError("quantity must be at least 1")return unit_cents * quantity
Type hints communicate the intended parameter and return types to readers, editors, and static checkers; Python does not enforce them automatically. A short docstring should explain purpose, units, or behavior that the signature cannot make obvious. It should not merely repeat the function name.
Design one focused responsibility
A focused function has a name that describes one job, receives the information it needs, and returns one predictable kind of result. Pure functions—functions whose result depends only on their arguments and which do not change external state—are especially easy to test and combine. Side effects such as printing, file access, and network requests are often clearer at the program's edges.
Prefer a small function that calculates and returns over one that calculates, prints, saves, and emails.
Keep validation close to the boundary that owns the rule.
Return one predictable type instead of sometimes returning data and sometimes an error string.
Extract a helper when a block has a clear name and independent contract.
Do not split code into tiny functions when the names add no clarity.
Common function mistakes
Calling print inside a calculation and expecting the printed text to be its return value.
Forgetting that a function without an explicit return produces None.
Using mutable list or dictionary defaults.
Hiding a stable parameter contract behind *args or **kwargs.
Reading or changing globals instead of passing dependencies explicitly.
Catching every exception inside the function and hiding the failure from its caller.
Writing a large function with several unrelated responsibilities.
Build a reliable order summary
Use the notebook below to trace a request through validation, calculation, and presentation. Predict the two valid summaries, run the program, then try a quantity of 0 to inspect the contract failure. Add a keyword-only delivery charge without changing the calculation's return type.
Identify each parameter and the argument bound to it.
Explain why calculate_total returns cents instead of printing text.
Call the function once with positional arguments and once with keyword arguments.
Trigger one ValueError and read its final message.
Add one focused test case by comparing an actual result with an expected result.
Lesson review
You can now define and call functions, distinguish parameters from arguments, return reusable data, design safe defaults, control how options are passed, reason about local scope, and state a clear contract with hints, documentation, validation, and exceptions. Complete the quick checks and scored quiz before continuing to Python collections.
RUNNABLE PYTHON NOTEBOOK
Build a reliable order summary
Run valid cases, change keyword-only options, and then trigger a validation error. Keep calculation, formatting, and orchestration as separate responsibilities.
defcalculate_total(
unit_cents:int,
quantity:int,*,
discount_percent:int=0,)->int:"""Return a validated order total in integer cents."""if unit_cents <0:raise ValueError("unit_cents must not be negative")if quantity <1:raise ValueError("quantity must be at least 1")ifnot0<= discount_percent <=100:raise ValueError("discount_percent must be from 0 to 100")
subtotal = unit_cents * quantity
discount = subtotal * discount_percent //100return subtotal - discount
defformat_mad(cents:int)->str:returnf"MAD {cents /100:,.2f}"deforder_summary(name:str, unit_cents:int, quantity:int,*, discount_percent:int=0)->str:
total = calculate_total(
unit_cents,
quantity,
discount_percent=discount_percent,)returnf"{name}: {quantity} item(s) — {format_mad(total)}"print(order_summary("Notebook",1_995,3))print(order_summary("Course bundle",5_000,2, discount_percent=10))assert calculate_total(1_000,2)==2_000assert calculate_total(1_000,2, discount_percent=25)==1_500
OUTPUT
Press Run code to execute this Python program in your browser.
QUICK CHECK
Test what you learned
Which statement sends a value back to a function's caller?
QUICK CHECK
Test what you learned
Which value is commonly used as the safe default sentinel before creating a new list?
QUICK CHECK
Test what you learned
Which symbol makes following parameters keyword-only in a function signature?
KNOWLEDGE QUIZ
Check your function-design mental model
Choose an answer for every question, then check the explanations. The goal is not to memorize syntax; it is to predict what Python will do and why.