Matheion

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
  • def binds the name function_name to 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) passes 3 into the first slot and 4 into 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 None pattern 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 f in front, like f"{greeting}, {name}!". Anything inside the { } is a variable (or expression) whose value is dropped into the text. So if greeting is "Hi" and name is "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 of name here."

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. 1.A function ends without any `return` statement. What does calling it produce?

  2. 2.Given `def twice(x): return x * 2`, what is `twice(twice(3))`?

  3. 3.Why is `def log_event(msg, events=[]):` a bug when you call it several times without passing `events`?

  4. 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. 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)?