🐍 Python for Data Engineers · Chapter 3
Functions: the unit of everything
A function is a name you give to a piece of thinking, so you never have to think it again.
Study this lesson free in the app →
The basics
A function is a named, reusable block of code: inputs go in, one result comes out. def is_billable(event): ... lets you write if is_billable(e): everywhere instead of repeating a four-part condition.
The problem it solves: without functions, the same logic is copy-pasted in five places, fixed in three, and wrong in two. A function puts the thinking in one place — fixable once, testable once, and named so the next reader sees why instead of how.
The mental model: a recipe card. Parameters are the ingredients, return is the sauce. One card, one job — it doesn't also set the table.
Words you must know 5
- Parameter / argument
- the parameter is the name in the
defline (fare); the argument is the actual value you pass (800). - Default argument
only_completed=Truemakes a parameter optional. Never use a list or dict as a default: it is created once and shared by every call.return- hands back a value and exits immediately. No
returnmeans the caller getsNone. - Scope
- names created inside a function die when it returns; they can't leak out or be clobbered from outside.
- Side effect
- anything the function changes outside itself: mutating an argument, writing a file, printing.
The rule that makes a function testable: everything it needs arrives through its parameters, and everything it produces leaves through return.
Explained
Before:
if e["status"] == "completed" and e.get("fare") and e["fare"] > 0:After:
if is_billable(e):The second version is not shorter for the sake of it. It is testable, reusable, and it says why rather than how — so the next reader (you, in March) doesn't have to re-derive the intent.
The recipe card
A cook who has made a sauce a hundred times writes it on a card: ingredients in, sauce out.
Once it's on the card, three things become true.
Anyone can make the sauce without understanding it. The card can be fixed in one place and every dish improves. And you can test the card — make it once, taste it, and stop worrying.
The parameters are the ingredients. The return is the sauce. And crucially: a good card doesn't also set the table, book the guests and send the invoice. One card, one job.
The anatomy
def total_fare(events, only_completed=True):
"""Sum the fares. Defaults to completed rides only."""
total = 0
for e in events:
if only_completed and e["status"] != "completed":
continue
total += e["fare"]
return total
# calling it
total_fare(events) # uses the default
total_fare(events, only_completed=False) # named argument — self-documentingdef— here comes a functionevents— a required parameteronly_completed=True— an optional one with a default- the
"""docstring"""— what it does, for a human return— hands a value back and exits immediately
A function with no return gives back None. That is a real value, and forgetting it is the most common beginner bug in this lesson.
Watch return exit early
The early-return shape — handle the odd cases first, leave the normal case last and unindented — is how experienced code reads.
Scope: what exists where
Names created inside a function are invisible outside it. That is a feature, not a limitation — it means a function can't accidentally break the rest of your program.
def process():
count = 5 # local — born and dies here
return count
process()
print(count) # NameError: name 'count' is not definedA function can read names from outside, but should not depend on them:
TAX = 1.05 # a constant is fine
def with_tax(fare):
return fare * TAX # ✅ reads a constant
def bad(fare):
return fare * rate # ❌ where does `rate` come from?The rule that makes functions testable: everything the function needs comes in through its parameters. A function that reaches out to grab a global, read a file, or check the clock is one you can't test without recreating its whole world.
Two bugs, one function
def add_tag(event, tags=[]):
tags.append("processed")
event["tags"] = tags
add_tag({"id": "e1"})
add_tag({"id": "e2"})
result = add_tag({"id": "e3"})
print(result)Gotchas
- Mutable default arguments —
def f(items=[])shares one list across all calls, forever - Forgetting
return— the function gives backNoneand the caller silently gets nothing - Printing instead of returning —
printshows a human;returngives the program something to use. A function that prints can't be tested or composed - Doing three things — if the name needs an "and", split it
- Depending on globals — makes the function untestable and the bug unfindable
- Mutating the argument — the caller's data changes underneath them, from a line that looks harmless
Parameters in, return out. Nothing sneaks in through the sides.
return exits immediately.
Never def f(x=[]). Use None and build inside.
Words to know
| Function | A named, reusable block that takes inputs and returns a result. |
|---|---|
| Parameter | The name a function gives to an input it expects. |
| Argument | The actual value passed in when the function is called. |
| Default argument | A parameter with a fallback value, making it optional. |
| return | Hands a value back to the caller and exits the function immediately. |
| Scope | The region where a name exists; a function's locals die when it returns. |
| Docstring | The string just under def, describing what the function does. |
| Guard clause | An early return handling an edge case, so the main logic stays unindented. |
| Side effect | Anything a function changes outside itself — mutating an argument, writing a file. |
In two minutes
- A function names a piece of logic so it is written, fixed and tested in one place.
- Parameters in, return out; return also exits the function immediately.
- No return statement means the caller receives None — the most common beginner bug.
- Return values rather than printing them; printed output can't be tested or reused.
- Default arguments are evaluated once — never use a mutable default like [] or {}; use None.
- Locals die at return; read constants from outside, but never depend on mutable globals.
- Passing a list doesn't copy it: mutating it changes the caller's list, rebinding the name doesn't.
- Guard clauses first, one job per function, and say the signature aloud in interviews.
Test yourself
1. def f(items=[]) — why is this a bug?
- Lists can't be defaults
- The list is created once at definition and shared by every call
- It is slower than using None
- It raises a SyntaxError
Show the answer
The list is created once at definition and shared by every call — Defaults are evaluated once, when the function is defined. Mutating that list leaks state between calls, forever.
2. A function has no return statement. What does the caller get?
- 0
- None
- an empty string
- an error
Show the answer
None — Python returns None implicitly. The caller gets a real value that silently isn't what they expected.
3. Why prefer return over print inside a function?
- print is slower
- A returned value can be tested and used by other code; printed output cannot
- print doesn't work in scripts
- return uses less memory
Show the answer
A returned value can be tested and used by other code; printed output cannot — print communicates with a human. return communicates with the program — it's what makes a function composable and testable.
4. What happens to a variable created inside a function when it returns?
- It becomes global
- It stops existing
- It keeps its value for the next call
- It is returned automatically
Show the answer
It stops existing — Locals are scoped to the call. That isolation is what stops a function from breaking the rest of the program.
5. def fee(base, surge=1.0): return base * surge. What does fee(100, surge=1.5) return?
- 100
- 150.0
- 1.5
- TypeError
Show the answer
150.0 — base is bound to 100, surge overrides its default with 1.5, and 100 * 1.5 = 150.0 (a float, because one operand is a float).
6. With the same fee, what does fee(surge=1.5) do?
- Returns 1.5
- Returns 0
- Raises TypeError: missing required argument 'base'
- Uses base=1.0
Show the answer
Raises TypeError: missing required argument 'base' — base has no default, so it is required. Defaults only exist for parameters that declare one.
There are 10 questions, 3 puzzles and 6 recall cards for this lesson in the app — the recall cards come back on later days, just before you would forget.
Start this lesson →