Functions
Our Bill Splitter has been repeating the same tip math everywhere — a function lets us name that logic once and reuse it, like a total_with_tip button the whole app can press. In this lesson we package restaurant logic into clean, testable functions with parameters, defaults, and return values. By the end you'll build the exact total_with_tip(subtotal, tip_percent=18) the checkout screen calls.
Every box is live — edit and ▶ Run.
Part 1 — def, parameters, and return
A function has four moving parts: def starts the definition, a name you'll call later, parameters in the parentheses (placeholders for incoming values), and return to hand a result back. Calling add_tip(40, 8) slots 40 and 8 into subtotal and tip, runs the body, and gives back whatever return produces.
The values you pass in (40, 8) are called arguments. Parameters are the names in the definition; arguments are the actual values at the call site.
Try it: Change the body to
return total * 2and Run. The whole program's output shifts because everything flows through that onereturn.
Part 2 — Positional vs keyword arguments
You can pass arguments two ways. Positional arguments are matched by order — the first value fills the first parameter. Keyword arguments name the parameter explicitly (tip_percent=18), so order stops mattering and the call reads like a sentence.
Both calls produce the same receipt, but the keyword version is self-documenting and immune to argument-order mistakes.
Watch out: Positional arguments must come before keyword ones.
receipt(subtotal=80, 18)is aSyntaxError— once you go keyword, you can't go back to positional in the same call.
Part 3 — Default parameter values
A default gives a parameter a fallback value used when the caller skips it. In a restaurant most guests tip 18%, so tip_percent=18 lets the "quick total" button work with a single argument — while still allowing an override.
A default is evaluated once, when the function is defined — not on each call.
With an immutable default like 18 you'll never notice. With a mutable one you
will, and it's one of Python's genuinely notorious traps:
def add_order(item, orders=[]):
orders.append(item)
return orders
add_order("soup")
print(add_order("cake"))The fix is to default to None and build a fresh list inside:
Defaults must come after all non-default parameters: def f(a, b=1) is valid, def f(a=1, b) is a SyntaxError.
Watch out: Never use a mutable default like
[]or{}. Python creates that object once at definition time and reuses it across every call, so state leaks between calls:
The fix is party=None inside the function, then if party is None: party = [].
❓ Cross-question — "In TypeScript
function f(p = [])makes a fresh array every call — not here?" Correct, and it's the #1 gotcha for TS/JS devs. A Python default is evaluated once, when the function is defined, and reused on every call — so a default[]is shared and accumulates across calls. Default toNoneand build the list inside.
Part 4 — A function with no return gives None
If a function never hits a return statement, Python hands back None automatically. This trips people up when they confuse printing a value with returning one — print shows text on screen but the function's value is still None.
Our graded total_with_tip must return the number (so the app can do math with it), not just print it.
Note:
returnwith no value (return) and noreturnat all both produceNone. Reach forreturnwhen a caller needs the result; use
Part 5 — Local vs global scope
A variable created inside a function is local — it lives only during that call and can't be seen from outside. Names defined at the top level of your file are global and are readable inside functions.
Reading a global is fine. But assigning to a name inside a function creates a brand-new local that shadows the global — it does not change the outer variable:
Watch out: To actually reassign a global from inside a function you'd need the
globalkeyword — but that's usually a code smell. Prefer passing values in as parameters and handing results back withreturn.
Part 6 — Returning multiple values with a tuple
return a, b packs both values into a tuple, and the caller can unpack them into separate names in one line. This is perfect for a bill splitter that needs the per-person share and the leftover cents.
If you don't unpack, you just get the tuple back whole — and its order is guaranteed (tuples are ordered), so indexing is safe:
Try it: Change
split_bill(100, 3)tosplit_bill(100, 7)and see how the per-person share and remainder both shift.
Part 7 — *args and **kwargs (intro)
Sometimes you don't know how many arguments will arrive. *args collects any extra positional arguments into a tuple; **kwargs collects any extra keyword arguments into a dict. Great for a "sum up every item on the table" helper.
**kwargs does the same for named arguments, handing you a dictionary. Since dict order of keys isn't something to assert loosely, we sort the keys for a deterministic result:
Note: The names
argsandkwargsare just convention — it's the*and**that do the magic. You'll meet these constantly when reading real-world library code.
Part 8 — Docstrings
A docstring is a string literal placed as the very first line inside a function. It documents what the function does, shows up in help(), and is available programmatically as function.__doc__. Professional code always documents its public functions this way.
Try it: Run
help(total_with_tip)in your head — the docstring is exactly what Python would show. Good docstrings state what the function returns and what its parameters mean.
Part 9 — Pythonic function idioms
You can define, call, and default parameters. Now the tools that make functions composable — what senior Python code leans on constantly.
lambda — a throwaway one-line function
lambda args: expression is an unnamed function you write inline, usually to hand
to another function:
Coming from Java/JS:
lambda x: x*xis JS'sx => x*x/ Java'sx -> x*x. A Python lambda is a single expression (no statements) — for anything bigger, use a normaldef.
Higher-order functions — pass a function as an argument
Functions are values: store them, pass them, return them. sorted(key=…),
map, and filter all take a function:
Keyword-only and positional-only parameters
A bare * forces everything after it to be passed by name — great for flags,
so call sites stay readable. A / forces everything before it to be positional:
Coming from Java/JS: Python has real keyword args — no need for an "options object" (
book(12, {vip:true})).book(12, vip=True)is built in.
❓ Cross-question — "How do I get a TypeScript-style options object with optional named fields?" You don't need one. Any parameter can be passed by name, a default makes it optional, and a bare
*makes flags keyword-only (sobook(12, True)is rejected). It's TS named args without the wrapper object.
Call-site unpacking — * and ** when calling
The same */** spread a list/dict into arguments at the call:
Coming from JS:
total(*nums)is spreadtotal(...nums).
❓ Cross-question — "Are
*args/**kwargsthe same as rest/spread?" Yes. Definingdef f(*args, **kwargs)≈ TS(...args)plus an options object; callingf(*list, **dict)≈f(...arr, ...obj).*handles positional/array,**handles keyword/object.
Closures — a function that remembers
A function defined inside another remembers the outer variables even after the outer returns. That remembered state makes a factory:
Reminder — the mutable-default trap (Part 3):
def f(items=[])shares ONE list across all calls. Default toNoneand create inside:items = items or [].
⚡ Advanced — decorators: wrap a function to add behaviour
A decorator takes a function and returns a new one — applied with @name above
a def. It's how logging, timing, caching, and auth get bolted on without touching
the body. functools.wraps keeps the wrapped function's name/docstring:
Coming from Java: closer to a Spring AOP aspect than a plain annotation — it actually wraps behaviour. From JS: the
@decorator/ HOC pattern.
❓ Cross-question — "Are these the same as TypeScript decorators?" Not quite. TS decorators are (still experimental) metadata/reflection hooks; a Python decorator is a plain function that takes a function and returns a replacement — a higher-order function applied with
@. Same idea as a React HOC or Express middleware: real behaviour-wrapping, runnable today.
⚡ Advanced — the functools toolkit
🎯 Your turn
Write total_with_tip(subtotal, tip_percent=18) — it returns the subtotal plus tip, rounded to two decimals (tip_percent defaults to 18):
total_with_tip(100)→118.0total_with_tip(100, 20)→120.0
Hint — give tip_percent a default of 18, then return round(subtotal + subtotal * tip_percent / 100, 2).
Then press ▶ Run, tap the Live App try chips to call it with different inputs, and hit ✓ Check. Green = this piece of the app is built. ✅
