Python from zero to data engineering Lesson 11 / 110

Functions: give a task a name and a clear boundary

Write functions with parameters and return values, separate calculation from input/output, and avoid hidden work.

We can calculate a receipt. Now we want the same calculation at the ticket desk and in a daily report. Copying the formula into both places works until a price rule changes and someone updates only one copy. The ghosts are not responsible for this kind of haunting.

You need: expressions, conditions, and collections. By the end: you can define a function, pass values to it, and use its result. Allow 30–40 minutes.

Define a task, then call it

Save as ticket_functions.py:

def calculate_total(ticket_count, price_cents):
    return ticket_count * price_cents


total = calculate_total(3, 850)
print(total)

Output:

2550

def defines a function. The names inside its parentheses are parameters: names for the information it needs. The values supplied when calling it, 3 and 850, are arguments. The function body is indented.

Executing def creates the function; it does not run the body. The call runs it. return ends that call and gives a value back to the caller. We assign that value to total.

Returning is not printing

def show_total(ticket_count, price_cents):
    print(ticket_count * price_cents)


result = show_total(3, 850)
print(result)

This prints 2550, then None. The first function displayed a number but did not return it explicitly. A function that reaches the end without return returns None.

Use a return value when another part of the program needs the result. Keep display decisions at the edge of the program where practical. A function that reads input, calculates a total, writes a file, and prints a receipt has several reasons to fail and is harder to reuse.

Make formatting reusable too

def format_euros(amount_cents):
    euros = amount_cents // 100
    cents = amount_cents % 100
    return f"EUR {euros}.{cents:02d}"


print(format_euros(2550))
print(format_euros(805))

Output:

EUR 25.50
EUR 8.05

For this course project, the function expects a non-negative integer cent amount. That expectation is part of its contract. Negative amounts need a deliberate formatting rule; do not silently reuse this function for refund displays without examining that case.

euros and cents are local names belonging to the function call. Code outside the function should use its returned value, not try to read those internal names. Scope and object mutation deserve a deeper lesson later.

Separate a rule from the action

def can_book(ticket_count, available_seats):
    return ticket_count > 0 and ticket_count <= available_seats


print(can_book(3, 12))
print(can_book(0, 12))
print(can_book(13, 12))

This prints True, False, False. The function answers a question; it does not alter the seat count. Its inputs are expected to be integers. Passing text instead is a different error from a valid integer that fails the booking rule.

A useful function does one coherent job. It need not be exactly one line. Prefer a small number of understandable tasks over dozens of wrappers that merely rename every operator.

Performance and engineering choices

Extracting a function does not automatically make code faster. Calling a function has overhead. The immediate benefit here is consistent behaviour and a boundary we can test without typing into input().

Watch for expensive repeated work inside a function. A helper called for each row should not secretly reload the same lookup file on every call. Load reusable inputs once at the appropriate level, then pass them in. Later we will measure costs and discuss caching, including its memory and freshness tradeoffs.

Avoid printing every record in a large transformation just because debugging output is convenient. Console output and stored logs can add work, volume, and—in some systems—cost. Small summaries often answer the operational question more directly.

Your turn: total a set of bookings

Write count_tickets(bookings), where each booking is a dictionary with a required tickets field. Return the combined count without printing inside the function. Test an empty list and bookings containing 2 and 3 tickets.

Show one solution
def count_tickets(bookings):
    total = 0
    for booking in bookings:
        total = total + booking["tickets"]
    return total


print(count_tickets([]))
print(count_tickets([{"tickets": 2}, {"tickets": 3}]))

Expected output: 0, then 5. return is outside the loop, so every record contributes. Putting it inside the loop would return after the first record and mishandle the empty case.

Checkpoint: explain what a caller receives, which names are local, and why a function that returns a calculation is easier to test than one that asks questions and prints everything itself.

Search