MA-PY1 · Module 1.2
Defining and calling functions
Topics: Functions · Scope · Return values
Overview
Module 1.2 — Defining and calling functions
Scroll Core ideas → worked snippets → Practice (quiz). Attempt each question before Discuss / reveal. Use any Python REPL, script, or notebook you like.
Concepts
Core ideas — functions as named, reusable computations
Functions are how you turn a recipe (“given these inputs, do these steps”) into something you can name, reuse, test, and read without drowning in repetition.
Anatomy of def
def function_name(parameters):
indented body — one or more statements
defbinds the namefunction_nameto a callable object (the running interpreter stores the bytecode and defaults there).- Parameters (formal) are the placeholders in parentheses: they exist only inside the definition.
- Arguments (actual) are the values you supply at call time:
hypotenuse(3, 4)passes3into the first slot and4into the second. - The body must be indented consistently—Python uses indentation to distinguish blocks (no
{}braces).
Calling conventions (foundation)
Call with positional arguments in order (dist(30, True)) or attach keywords (dist(speed_kmh=30, raining=True)). Keywords are ideal when callers might forget ordering or when booleans alone are cryptic (retry=False reads better than a bare False at position three).
Mixed calls must keep positional first, then keywords—you’ll deepen this when you hit default parameters and unpacked collections later.
return vs print
| Pattern | Meaning |
|--------|---------|
| return x | The caller receives x; the function stops immediately for that invocation. |
| print(x) | Writes human-readable bytes to stdout; callers get None back from print itself. |
A function meant to compute a value almost always returns it so other code can compose (square(hypotenuse(a, b))). Debugging **print**s inside helpers are okay temporarily—delete or gate them once logic is stable.
The implicit return: if execution “falls off” the bottom without return, Python returns None—the single object of type NoneType. That is easy to miss when you forget return result in one branch.
Local names and the “where does this live?” question
Assignments inside a function create local names by default. They shadow any same-named global for the duration of the call. You do not need global to read a global constant; you need it only if you assign to a global name from inside a function—a pattern you should avoid until you clearly need it.
This keeps functions modular: their inner variables don’t clutter the outer namespace.
Default parameters
Syntax: def f(a, b=0):. Defaults are evaluated once when def runs, not on every call—a detail that bites when the default is mutable:
def bad(acc=[]): # DON'T — shared list across calls
acc.append(1)
return acc
Prefer None sentinel + fresh list per call:
def good(acc=None):
if acc is None:
acc = []
acc.append(1)
return acc
Pure vs effectful helpers
- Pure (given the same inputs, same output; no hidden I/O): easiest to reason about and unit test.
- Effectful (mutates globals, prints, reads files): still useful, but document them and keep the “main effect” obvious.
Early programs should lean pure plus one clear main() orchestrator.
Documentation and names
A docstring (first string literal in the body, often triple-quoted) records purpose, parameters, and return value. Tools and help() surface it—your future self is the primary audience.
Names should sound like verbs for actions (normalise_email) and nouns for data (rows). Short scripts may use x in two lines; anything longer earns descriptive names.
Self-study checklist
- Rewrite one small script fragment so every intermediate value **
return**s through a helper instead of printing. - Explain aloud: parameter vs argument vs local variable.
- Memorise the mutable default pitfall and the
Nonepattern to fix it.
Examples
Worked snippets — building function muscle
Basic define / call / return
def square(x):
return x * x
def sum_of_squares(a, b):
return square(a) + square(b)
print(sum_of_squares(3, 4)) # 9 + 16
The caller chains results; square never prints—good separation.
Early returns and guard clauses
def classify_sign(x):
"""Return 'neg', 'zero', or 'pos'."""
if x < 0:
return "neg"
if x > 0:
return "pos"
return "zero"
Each branch leaves immediately—no deep else pyramid.
Forgetting return (common bug)
def broken_double(x):
x * 2 # computed then discarded
def fixed_double(x):
return x * 2
print(broken_double(5), fixed_double(5)) # None 10
Use REPL or temporary print/debug to catch None leaks.
Keyword arguments for readability
def launch_delay(minutes, urgent):
if urgent:
return max(0, minutes - 5)
return minutes
print(launch_delay(12, urgent=False))
print(launch_delay(minutes=12, urgent=True))
Default parameters (safe pattern)
Quick note — f-strings. The next example uses an f-string: a string with an
fin front, likef"{greeting}, {name}!". Anything inside the{ }is a variable (or expression) whose value is dropped into the text. So ifgreetingis"Hi"andnameis"Lin", the f-string becomes"Hi, Lin!". You will study strings in depth a little later; for now just read{name}as "put the value ofnamehere."
def greet(name, greeting="Hello"):
return f"{greeting}, {name}!"
print(greet("Ada"))
print(greet("Lin", "Hi"))
Mutable-default fix (pattern to copy)
def append_item(item, bucket=None):
if bucket is None:
bucket = []
bucket.append(item)
return bucket
a = append_item(1)
b = append_item(2)
print(a, b) # [1] [2] — independent lists
Returning multiple values (tuple unpacking)
def divmod_like(a, b):
return a // b, a % b # comma builds a tuple
q, r = divmod_like(17, 5)
print(q, r)
Callers can ignore parts with _ if needed: q, _ = divmod_like(17, 5).
Nested calls and mental evaluation
def clamp(x, lo, hi):
return max(lo, min(hi, x))
print(clamp(1.5, 0, 1)) # 1.0
Trace inner min then outer max on paper before running.
Docstring + type hints (optional but professional)
def hypotenuse(a: float, b: float) -> float:
"""Return sqrt(a^2 + b^2), rounded to three decimals."""
h = (a * a + b * b) ** 0.5
return round(h, 3)
print(hypotenuse(3.0, 4.0))
Hints are not enforced at runtime by default—they guide readers and static checkers.
Practice check — defining and calling functions
Multiple-choice check from the Matheion app lesson. Pick an answer for each question, then submit.
1.A function ends without any `return` statement. What does calling it produce?
2.Given `def twice(x): return x * 2`, what is `twice(twice(3))`?
3.Why is `def log_event(msg, events=[]):` a bug when you call it several times without passing `events`?
4.You want to use a function's result in further arithmetic, like `total = area(3) + area(5)`. Should the function `print` the value or `return` it?
5.Inside a function you write `count = 5`, where `count` also exists outside the function. After the call, what is the outer `count` (assuming no `global` declaration)?