Pebellslearn by building
0%
RoadmapMatrixCatalogSandboxSign in
Fundamentals  ›  Lesson

Functions

Functions 15 minPackaging logic with parameters, defaults, and return values
You're building a piece ofBill Splitter & Tip Calculator
This piece — total_with_tip(): Adds a default 18% tip to any subtotal in one call.
Scenario Most guests tip 18%, so the 'quick total' button adds 18% automatically — unless someone picks a different amount.
Your task
Build total_with_tip(subtotal, tip_percent=18). It returns the subtotal plus tip, rounded to two decimals (tip_percent defaults to 18). Example: total_with_tip(100) → 118.0.

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.

python

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 * 2 and Run. The whole program's output shifts because everything flows through that one return.

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.

python

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 a SyntaxError — 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.

python

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:

What does this print?
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:

python

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:

python

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 to None and 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.

python

Our graded total_with_tip must return the number (so the app can do math with it), not just print it.

Note: return with no value (return) and no return at all both produce None. Reach for return when a caller needs the result; use print only for showing things to a human.

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.

python

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:

python

Watch out: To actually reassign a global from inside a function you'd need the global keyword — but that's usually a code smell. Prefer passing values in as parameters and handing results back with return.

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.

python

If you don't unpack, you just get the tuple back whole — and its order is guaranteed (tuples are ordered), so indexing is safe:

python

Try it: Change split_bill(100, 3) to split_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.

python

**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:

python

Note: The names args and kwargs are 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.

python

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:

python

Coming from Java/JS: lambda x: x*x is JS's x => x*x / Java's x -> x*x. A Python lambda is a single expression (no statements) — for anything bigger, use a normal def.

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:

python

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:

python

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 (so book(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:

python

Coming from JS: total(*nums) is spread total(...nums).

❓ Cross-question — "Are *args/**kwargs the same as rest/spread?" Yes. Defining def f(*args, **kwargs) ≈ TS (...args) plus an options object; calling f(*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:

python

Reminder — the mutable-default trap (Part 3): def f(items=[]) shares ONE list across all calls. Default to None and 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:

python

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

python

🎯 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.0
  • total_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. ✅

Practice — level up

0/5 solved

Solve each in its own editor — Run to try, Check to grade. Stuck? Reveal a Hint, or the full Solution + walkthrough.

Warm-upGreeting with a default

Write greet(name, greeting="Hello") returning f"{greeting}, {name}!". The greeting defaults to Hello.

default-argsf-strings
DrillSum any number of args

Write total(*nums) that returns the sum of however many numbers it's called with (total() is 0).

*args
BuildKeyword-only booking

Write book_table(size, *, smoking=False, window=False) — the flags after * must be passed by name. Return f"table for {size}", and if any flags are on, append them in () joined by , (smoking before window).

keyword-onlyconditionalsf-strings
BossTransform by name

Write transform(nums, op) where op is "double", "square", or "negate". Return a new list with that operation applied to each number. Use a dict of lambdas.

lambdahigher-ordercomprehension
CapstoneSum a tree

A tree node is {"value": int, "children": [<nodes>]}. Write tree_sum(node) returning the sum of every value in the whole tree (recursively).

recursiondictgenerator
total_with_tip.py
Call the function you wrote — just like your app's frontend would. Edit the inputs and hit Call.
total_with_tip(subtotal, tip_percent) → floatSubtotal plus tip. tip_percent defaults to 18.
try:
returned
Press Run to execute your code.
Press Check to run the tests for this lesson.