Pebellslearn by building
0%
RoadmapMatrixCatalogSandboxSign in
Fundamentals  ›  Lesson

Exceptions & Errors

Exceptions 13 minHandling things that go wrong with try / except
You're building a piece ofBill Splitter & Tip Calculator
This piece — safe_split(): Splits the total between people without ever crashing.
Scenario Someone clears the 'people' field to 0 mid-edit. Dividing by 0 would crash the app — this stays calm and returns 0.0.
Your task
Build safe_split(total, people). It returns each person's share, or 0.0 if people is 0 (it never crashes). Example: safe_split(100, 4) → 25.0.

Exceptions & Errors — Keeping the App Calm When Things Go Wrong

Our Bill Splitter runs on live, half-typed input: someone clears the people field to 0, pastes "12%" into a number box, or taps a menu item that isn't there. Any one of those can make Python raise an exception and crash the whole app — so this lesson is about catching the ones we expect, letting real bugs surface, and deciding when to guard versus when to shout. It builds straight toward your safe_split(total, people).

Every box is live — edit and ▶ Run.


Part 1 — What "raises" means: the four errors you'll meet most

When Python hits an operation it genuinely cannot perform, it raises an exception — an object with a name (the type) and a message. Unhandled, the exception unwinds the program and prints a traceback. The name tells you what went wrong, so learn to recognise the common ones. Here are the four that show up constantly in a bill-splitting app:

100 / 0        # 💥 ZeroDivisionError: division by zero
int("four")    # 💥 ValueError: invalid literal for int() with base 10
[10, 20][5]    # 💥 IndexError: list index out of range
{"food": 84}["tip"]   # 💥 KeyError: 'tip'

Instead of crashing, let's catch each one and just print its type name so we can compare them side by side:

python

Note: type(e).__name__ gives you the exception's class name as a string — handy for logging. Each of these is a subclass of Exception, which is why one broad except Exception caught them all here.

Try it: Add a fifth attempt — ("bad type", lambda: "3" + 5) — and rerun. What name does mixing a str and an int raise?


Part 2 — try / except: catch it and carry on

Wrap the risky line in try:. If it raises, Python skips the rest of the try block and runs the matching except: instead of crashing. This is the exact shape behind safe_split — someone empties the people box, people becomes 0, and dividing would blow up:

python

The program keeps running and shows a calm 0.0 instead of a stack trace. That's the whole point: an expected, recoverable problem should not end the app.

Try it: Change people to 4. The try block succeeds, the except is skipped entirely, and share becomes 25.0.


Part 3 — Read the message with except ... as e

The exception object carries a human-readable message. Capture it with as e and you can log it, show it, or inspect it. This is gold when parsing user input, because the message tells you exactly what Python choked on:

python

Note: Printing an exception prints its message, not its type. If you want the type too, use f"{type(e).__name__}: {e}".

Try it: Swap "12%" for "12.5" — it parses fine and the except never fires (nothing prints). Then try "$8" to see the message change.


Part 4 — Catch a SPECIFIC exception, not a broad one

Name the exact exception you expect. Catching ValueError handles bad text while letting unexpected failures still crash loudly — which is what you want, because a crash you can see beats a bug you can't:

python

Now watch what a lazy broad except Exception: does. Here there's a typosubtotl instead of subtotal — which raises NameError. A broad catch swallows it and returns a wrong-but-quiet answer, so the bug ships:

python

The tip silently vanished and nobody noticed. A specific except ValueError: would have let that NameError crash the app during testing, where you'd fix it.

Watch out: A bare except: (no type at all) is even worse — it also catches KeyboardInterrupt and SystemExit, so you can't even Ctrl-C out. Never write a bare except:. Catch the narrowest type that makes sense.


Part 5 — The full shape: try / except / else / finally

Two optional clauses complete the picture. else: runs only when the try succeeded (no exception) — the place for code that depends on the risky step working. finally: runs no matter what — success, handled error, even an early return — so it's for cleanup that must always happen:

python

Notice finally printed after else and before the function actually returned — Python guarantees it runs on the way out.

That guarantee is stronger than most people expect. finally runs even when the try block already decided to return:

What does this print?
def f():
    try:
        return "from try"
    finally:
        print("cleanup, then ", end="")

print(f())

Step through it if that ordering feels wrong — watching the frame return makes it click:

step through it
1def f():
2 try:
3 return "from try"
4 finally:
5 print("cleanup")
6 
7result = f()
8print(result)

Try it: Call read_amount("oops") instead. Now the except branch runs, then finally still fires — the order becomes not a number then (input handled).

Note: Why else instead of putting that code in the try? So a ValueError from later code doesn't get mis-caught by the except ValueError: meant for float(). Keep try blocks as small as the risky operation.


Part 6 — Raising your own with raise ValueError("msg")

You don't only catch exceptions — you can raise them to reject bad input at the source. raise ValueError("...") stops the function and hands a clear message to whoever called it. This is how you enforce rules like "a table needs at least one person":

python

Choose a type that fits the problem: ValueError for a value that's the wrong shape/content, TypeError for the wrong type, KeyError/IndexError for missing lookups. A precise type + a clear message makes your function honest about what it needs.

Try it: Call set_people(3) inside the try instead. It returns 3 with no exception, so the except is skipped and nothing prints.


Part 7 — finally for cleanup that must always run

The killer use of finally is releasing a resource — closing a file, a network connection, or an open tab — whether or not the work succeeded. Here a charge might fail, but the tab must close either way:

python

The tab closed before the exception propagated up to the outer handler. Without finally, that raise would have skipped the cleanup and leaked the resource.

Note: In real code you'd usually use a with statement (a context manager) for files and connections — it's finally cleanup done automatically. But knowing the underlying finally guarantee is what makes with click.


Part 8 — The design decision: guard vs. crash

Same broken input, two valid strategies. Guard (catch and return a safe default) when the caller is a live UI that should stay calm. Raise when the caller passed something genuinely invalid and should be told to fix it. Compare a strict version against the guarded safe_split you're about to build:

python

For our playground app the answer is guard: a half-typed form with 0 people shouldn't crash the screen — it should quietly show 0.0 until the user finishes typing. That's exactly the behaviour safe_split needs.

Watch out: "Guard everything" is not a rule — it's a trade-off. Guarding hides problems, which is great for transient UI states and terrible for programmer mistakes. Guard the expected, recoverable cases (empty field → 0.0); let the unexpected ones crash so you find them.


Part 9 — Idioms & real-world patterns

You can try/except/else/finally and raise. Here's how exceptions look in real Python codebases.

Catch several types at once

Group related errors in one except with a tuple:

python

Custom exceptions — name your failures

Subclass Exception so callers can catch exactly your error and not swallow unrelated ones:

python

Coming from Java: like a custom exception class — but Python never forces you to declare (throws) or catch it.

❓ Cross-question — "Are these checked exceptions?" No — Python has no checked exceptions and no throws clause (same as TypeScript). You catch by type (except ValueError), catch several with a tuple except (A, B), or broadly with except Exception. Nothing forces you to handle anything; unhandled errors just propagate up.

with — context managers clean up for you

A with block guarantees teardown even if the body raises. Write your own with contextlib.contextmanager:

python

Coming from Java/JS: with is Java's try-with-resources / C#'s using — it replaces the manual try/finally: close() dance.

❓ Cross-question — "Is with the new TypeScript using?" Same idea — deterministic cleanup, exactly like TS 5.2's using + Symbol.dispose. The object's __enter__/__exit__ run on entry/exit even if the body throws, so the resource is always released.

EAFP vs LBYL — the Python philosophy

  • LBYL ("Look Before You Leap"): check first — if key in d: ...
  • EAFP ("Easier to Ask Forgiveness than Permission"): just try it, handle the failure. Python prefers EAFP — often faster and race-condition-free:
python

❓ Cross-question — "Isn't try only for exceptional cases? I'd validate first." That's the LBYL habit from many languages. Python leans EAFP — try the operation, handle the failure — because it's often faster and avoids a check-then-act race (the key could vanish between the if and the access). Reach for EAFP first.

Real-world — parse a batch, collecting failures instead of crashing on the first bad row:

python

⚡ Advanced — chaining and re-raising

raise New(...) from e keeps the original cause in the traceback. A bare raise re-throws the current exception unchanged (log, then let it propagate):

python

❓ Cross-question — "Is raise X from e like throw new Error(msg, {cause: e})?" Exactly — it attaches the original error as the cause (e.__cause__), so the traceback shows both layers. To re-throw the current error unchanged, use a bare raise (like a bare throw;) — it preserves the original stack.

Gotcha: a return inside finally swallows any exception or return from the try body. Keep finally for cleanup only — never return from it.


🎯 Your turn

Write safe_split(total, people) — it returns each person's share, or 0.0 if people is 0 (it never crashes):

  • safe_split(100, 4)25.0
  • safe_split(100, 0)0.0

Hint — put the division in try: and catch except ZeroDivisionError: returning 0.0.

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-upSafe divide

Write safe_div(a, b) returning a / b, or 0.0 if b is zero.

try/except
DrillTolerant float parse

Write to_float(text) returning float(text), or None if it's not a valid number OR is None. Catch both errors in one except.

tuple-exceptcast
BuildDivide a batch

Write safe_divide_all(pairs) where each pair is [a, b]. Return a list of a / b, using None for any pair that divides by zero.

looptry/exceptlist
BossParse config lines

Write parse_settings(lines) for "key=value" lines. Return {"settings": {...}, "errors": [...]}: values that look like whole numbers become int, others stay strings, and any line without = goes into errors (kept as-is).

loopsplitcastdictguard
CapstoneSafe order batch

Each row is {"price": <str>, "qty": <str>}. Write safe_batch(rows) returning {"totals": [...], "errors": [...]}round(price*qty, 2) per good row (price→float, qty→int), and the index of any row that fails.

loopenumeratecasttuple-exceptdict
safe_split.py
Call the function you wrote — just like your app's frontend would. Edit the inputs and hit Call.
safe_split(total, people) → floatPer-person share, or 0.0 if people is 0.
try:
returned
Press Run to execute your code.
Press Check to run the tests for this lesson.