A function is a contract: give me THESE inputs, get back THIS output. Fuzzy contracts cause the bugs that eat afternoons — wrong argument order, forgotten returns, a None where a number should be. This lesson makes you precise about both sides of the arrow.
What you'll learn
What
Parameters are the names a function declares; arguments are the values a caller supplies, matched by position. return ends the call and delivers a value — or several, or None if you don't return at all.
Why
Data pipelines are functions wired output-to-input. One function quietly returning None, or arguments passed in the wrong order, breaks the chain in ways that surface far from the cause. Understanding the contract is understanding where pipelines break.
Where it's used
Every function you write or call: sklearn's fit(X, y) argument order, functions returning (train, test) pairs, validators returning a value or None.
Where this runs in production
The most-called line in ML — X_train, X_test, y_train, y_test = train_test_split(X, y) — is a function returning four values as a tuple, unpacked in one assignment.
Message-parsing functions return the parsed object or None; callers write `if parsed:` — the value-or-None contract keeps malformed input from crashing channels.
Factory calibration software chains conversion functions, each output feeding the next input — one function's misordered arguments would mis-calibrate a production line.
A paper form has labeled boxes in fixed positions: surname first, then given name. Fill them in the wrong order and the airline happily prints a boarding pass for 'Ada Lovelace' reversed — the SYSTEM accepted it; the MEANING broke. Positional arguments are those boxes: the function trusts you filled them in declaration order.
def describe(value, decimals):
return f'{round(value, decimals)}'
print(describe(3.14159, 2)) # 3.14 — correct order
print(describe(2, 3.14159)) # 2 — legal, meaninglessBoth calls run. Only one is right. When a function grows past two or three parameters, this fragility is why keyword arguments exist — next lesson's topic.
return does two jobs at once: it delivers a value AND terminates the function on the spot. Any code after an executed return never runs. That's not a limitation — it's the tool: 'early returns' handle edge cases at the top (return the answer for the easy case now) so the main logic below stays flat. You met this shape as guard clauses; return is what powers them.
def safe_average(values):
if len(values) == 0:
return 0.0
return sum(values) / len(values)
print(safe_average([10, 20, 30])) # 20.0
print(safe_average([])) # 0.0 — no crashTwo returns, one function: the guard exits early for the empty case (preventing a division by zero), and the main return handles real data. Multiple return statements are normal, healthy Python.
Passing the wrong NUMBER of arguments fails loudly: 'missing 1 required positional argument' or 'takes 2 but 3 were given' — read these carefully, they name the function and count. Passing the wrong ORDER with compatible types fails silently — no error, wrong result. Loud failures cost minutes; silent ones cost afternoons. Defend with clear parameter names, few parameters, and (next lesson) keyword arguments.
split_bill(96.0, 3) from both sides of the arrow. Click each stage.
Compute the price per unit for a bulk purchase.
Two positional parameters. The function's whole job fits in the return line.
Your task
Write grade_stats(scores) that returns THREE values: the average (rounded to 1 decimal), the highest score, and how many scores passed (60+). Guard the empty-list case by returning (0.0, 0, 0). Unpack and print the results for the class list provided.
Average: 69.7 Top: 91 Passed: 4
Write your solution in the editor on the right, then hit Run.
def area(width, height): return width * height — what does area(3, 4) bind?
What does a function return if execution reaches the end of its body without any return statement?
def stats(xs): return min(xs), max(xs) — what TYPE does stats([3, 1, 4]) evaluate to?
What's the correct test?
Write bmi(weight_kg, height_m) returning the BMI rounded to 1 decimal (weight / height²). Print bmi(70.0, 1.75). Expected output: 22.9
def check(n): if n > 10: return 'big' print('small') What does `result = check(3)` leave in result?
Write classify_temp(celsius) using early returns: return 'invalid' if celsius is None, 'freezing' below 0, 'cold' below 15, 'warm' below 28, else 'hot'. Print the results for None, -4, 22, 31 (one per line). Expected: invalid freezing warm hot
What's the difference between a parameter and an argument?
A parameter is the name declared in the function definition — the labeled empty box. An argument is the actual value a caller supplies — what goes into the box, matched by position by default. The distinction sounds pedantic but pays off when reading error messages: 'missing 1 required positional argument: height' names the PARAMETER that no ARGUMENT filled, telling you exactly which box is empty. It also frames the deeper point that the function body only ever sees its parameter names — callers and callees are decoupled, communicating purely through the ordered handoff of values.
How does Python return multiple values, and what's actually happening under the hood?
return a, b, c packs the values into a single tuple — the function still returns exactly one object. The multi-value feel comes from unpacking at the call site: lo, hi, span = value_range(data) assigns the tuple's elements to three names, and raises a loud ValueError if the counts mismatch. This is the idiom behind sklearn's train_test_split returning four arrays. Two design cautions worth volunteering: keep the element order meaningful and documented (callers must remember it), and past three or four values, return something with named fields instead — a dict or a small class — because positional unpacking of six anonymous values is a bug factory.
Describe the 'return value or None' contract — its uses and its hazards.
The contract: a function returns the meaningful result when it can, and None when it can't — parse_price returns a float or None, a lookup returns the record or None. It keeps expected failures (malformed input, missing keys) from becoming exceptions, letting callers handle them with a simple test. Two hazards. First, the truthiness trap: `if result:` wrongly treats legitimate falsy results — 0, empty string, empty list — as failures; the correct test is `is not None`. Second, None is contagious: if a caller forgets to check, the None flows onward and detonates later as a confusing TypeError far from its source. Teams mitigate with docstrings that state the contract, `is not None` discipline, and — in typed codebases — Optional annotations so the checker forces the question.
Common Mistakes to Avoid
1) Computing a result and forgetting the return line — caller gets None. 2) Swapping compatible-typed arguments: no error, wrong answer; keep parameter lists short and meaningful. 3) Testing a value-or-None result with truthiness when 0 or '' are legitimate — use `is not None`. 4) Code placed after a return in the same branch — unreachable. 5) Unpacking the wrong number of values — count both sides. 6) Believing print inside the function 'returned' something; the caller still received None.
Ask the AI Tutor
Try these prompts in the AI Tutor panel: • 'Quiz me: what does each of these five calls evaluate to?' • 'Show a silent argument-swap bug in a real-looking function.' • 'Refactor this nested if/else into early returns with me.' • 'Explain the value-or-None contract with a new example.' • 'Interview mode: ask me about multiple return values and grade my answer.'
Glossary
Parameter — the name declared in a def's parentheses. Argument — the value supplied at the call. Positional matching — first argument to first parameter, by order. return — statement that ends the call and delivers a value. Early return — exiting at the top for edge cases (guard clauses). Tuple packing — return a, b bundles values into one tuple. Unpacking — lo, hi = f() splits a returned tuple into names. None — the value of a function that returns nothing. Arity — the number of arguments a function expects. Value-or-None contract — return the result, or None for expected failure.
Recommended Resources
• Docs: 'More on Defining Functions' in the official Python tutorial. • Read: the train_test_split docs — spot the four-way tuple return and the parameter order you now understand. • Practice: write min_max_mean(values) returning three values with an empty-list guard, and unpack it three different ways. • Next in DSM: positional order is fragile past two parameters — Default & Keyword Arguments lets callers name what they pass and lets functions ship sensible defaults.
Recap
✓ Arguments fill parameters by position; order is meaning, and silent swaps are the danger. ✓ return delivers a value AND exits immediately — early returns keep logic flat. ✓ return a, b, c packs a tuple; unpack with matching names at the call site. ✓ No return means None — sometimes a contract, often a bug. ✓ Test value-or-None results with `is not None`, never bare truthiness, when falsy values are legitimate. ✓ Loud arity errors beat silent order errors — design parameter lists to fail loud. Next up: Default & Keyword Arguments. Callers shouldn't have to pass everything every time — defaults give parameters fallback values, and keywords let calls name their intent.
Run your code to see the output here.