◐ ai-teacher

🐍 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 def line (fare); the argument is the actual value you pass (800).
Default argument
only_completed=True makes 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 return means the caller gets None.
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-documenting

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 defined

A 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

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

FunctionA named, reusable block that takes inputs and returns a result.
ParameterThe name a function gives to an input it expects.
ArgumentThe actual value passed in when the function is called.
Default argumentA parameter with a fallback value, making it optional.
returnHands a value back to the caller and exits the function immediately.
ScopeThe region where a name exists; a function's locals die when it returns.
DocstringThe string just under def, describing what the function does.
Guard clauseAn early return handling an edge case, so the main logic stays unindented.
Side effectAnything a function changes outside itself — mutating an argument, writing a file.

In two minutes

Test yourself

1. def f(items=[]) — why is this a bug?

  1. Lists can't be defaults
  2. The list is created once at definition and shared by every call
  3. It is slower than using None
  4. 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?

  1. 0
  2. None
  3. an empty string
  4. 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?

  1. print is slower
  2. A returned value can be tested and used by other code; printed output cannot
  3. print doesn't work in scripts
  4. 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?

  1. It becomes global
  2. It stops existing
  3. It keeps its value for the next call
  4. 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?

  1. 100
  2. 150.0
  3. 1.5
  4. 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?

  1. Returns 1.5
  2. Returns 0
  3. Raises TypeError: missing required argument 'base'
  4. 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.

Learn it so it stays. Read it, test yourself, then recall it on two different days. The app schedules that for you, free.

Start this lesson →