DSM
60 XP
25 minsIntermediate60 XP

Raising Exceptions

Your validating setter already does it: raise ValueError('radius must be positive'). That one line is API design — it decides what callers can catch, what the 3am traceback says, and whether the bug is found in a minute or a week. Catching well is defense; raising well is how you build things others can defend against.

What you'll learn

  • Raise built-in exceptions with type and message chosen deliberately
  • Apply fail-fast: validate at boundaries, crash before corrupting
  • Define custom exception classes (and small hierarchies)
  • Re-raise correctly: bare raise, and raise X from e
  • Write error messages that name the value, the rule, and the fix

What

The raise statement signals failure deliberately: raise ValueError(f'...'). This lesson covers choosing the right built-in type, writing messages with the offending values, defining custom exception classes for your domain, re-raising with `raise` and `raise ... from`, and failing fast at boundaries.

Why

Every function you publish has an error contract, stated or not. Precise raises make failures debuggable (the message names the culprit), routable (callers catch by type), and early (bad state stops at the door instead of corrupting downstream). Libraries you admire — pandas, Stripe, requests — are admired partly for HOW they fail.

Where it's used

Validation gates (setters, config loaders), guard clauses in functions, library/API design, and wrapping low-level errors in domain terms.

Where this runs in production

StripeAn exception hierarchy as public API

StripeError → CardError, RateLimitError, AuthenticationError... — documented types integrators catch selectively. The hierarchy IS the error contract, versioned like any API.

pandasDomain-specific failures

pandas defines MergeError, EmptyDataError, and friends — so `except pd.errors.EmptyDataError` reads as data logic, not generic plumbing.

SpaceXFail fast at the boundary

Telemetry validators reject out-of-range readings at ingestion — a loud rejection at the gate beats a quiet wrong number in a burn calculation. Fail-fast is aerospace doctrine first.

Theory

The core ideas, in plain language.

raise ExceptionType('message') constructs and throws in one line. From there the machinery is last lesson's: unwinding, matching, handling. Your choices are the TYPE (what callers will catch) and the MESSAGE (what the human at the traceback reads). Both are design decisions, not afterthoughts.
def set_threshold(value):
    if not isinstance(value, (int, float)):
        raise TypeError(f'threshold must be a number, got {type(value).__name__}')
    if not 0 <= value <= 1:
        raise ValueError(f'threshold must be in [0, 1], got {value}')
    return value

print(set_threshold(0.85))
# set_threshold(1.5)   -> ValueError: threshold must be in [0, 1], got 1.5
# set_threshold('hi')  -> TypeError: threshold must be a number, got str

The built-in split you'll use daily: TypeError for wrong KIND of thing, ValueError for right kind, unacceptable VALUE. And the message formula: the rule, plus the offending value. 'got 1.5' turns a mystery into a one-minute fix.

Analogy: The bouncer at the door

Fail-fast is a nightclub bouncer. Checking IDs at the DOOR is cheap: the underage guest is turned away with a clear reason, and everyone inside is known-valid. The alternative — letting everyone in and discovering problems at the bar, on the dance floor, at 2am — means the incident happens far from the cause, with no ID in hand. A function that validates its inputs up front and raises immediately IS that bouncer: every line after the guard clauses runs in a known-good world, and the traceback points at the door, not the dance floor.

Key Concept
Choosing the type: match the caller's question

Pick the type by what a CALLER would want to catch: ValueError (bad value), TypeError (bad type), KeyError/LookupError (missing thing), NotImplementedError (subclass must override — your template methods already raise it), RuntimeError (valid inputs, invalid state — 'transform before fit'). Never raise bare Exception: it forces callers into the blanket catch you learned to avoid. If no built-in matches the caller's question, that's the signal for a custom class.

class PipelineError(Exception):
    """Base for this pipeline's failures."""

class SchemaError(PipelineError):
    """Input data doesn't match the expected schema."""

class SourceUnavailableError(PipelineError):
    """An upstream source can't be reached."""

# callers choose their granularity:
#   except SchemaError:            handle just schema problems
#   except PipelineError:          handle anything ours
# ...while a NameError bug still crashes loudly. That's the win.

A custom hierarchy is three lines per class — inherit from Exception (your OOP module doing real work), write a docstring, done. The base class gives callers a catch-all for YOUR failures that still excludes genuine bugs — the precision blanket `except Exception` can never offer.

Key Concept
Re-raising: bare raise, and raise ... from

Inside an except block, bare `raise` re-throws the SAME exception — for when you want to log-and-propagate, not handle. `raise SchemaError('orders feed invalid') from e` throws a NEW, higher-level exception while CHAINING the original: the traceback shows both, joined by 'The above exception was the direct cause...'. Translation without evidence destruction — wrap low-level errors (KeyError) in domain terms (SchemaError) and the root cause stays in the log.

Watch out
Message quality is incident-response quality

The message is read exactly once — at the worst possible time, by someone (possibly future-you) with no context. Include: what rule was violated, the offending VALUE (repr it: {value!r} makes '' and None visible), and identifiers that locate the culprit (row id, column name, filename). 'Invalid input' is a taunt; "amount must be >= 0, got -3.2 (order_id='A-4471')" is a fix. One caution: never put secrets (tokens, passwords) in messages — tracebacks end up in logs.

Visual Learning

See the concept, then explore it.

What should this function raise?

The decision path from 'something is wrong' to the right raise. Click each node.

Worked Examples

Watch it built up, one line at a time.

Very EasyFirst deliberate raise

A withdrawal that refuses the impossible.

Step 1 of 1

Guard clauses first (the early-return shape from Functions, now raising), happy path after — every line below the guards runs in a validated world. Both messages carry the numbers a debugger needs.

Code
01def withdraw(balance, amount):
02 if amount <= 0:
03 raise ValueError(f'amount must be positive, got {amount}')
04 if amount > balance:
05 raise ValueError(f'insufficient funds: balance {balance}, requested {amount}')
06 return balance - amount
07 
08print(withdraw(100.0, 30.0))
09try:
10 withdraw(100.0, 250.0)
11except ValueError as e:
12 print(f'rejected: {e}')
Output
70.0
rejected: insufficient funds: balance 100.0, requested 250.0
Practice Coding

Your turn — write the code.

Your task

Build a registration gate. Define InvalidUsernameError(Exception). register(name) raises it if the name is under 3 chars ('too short: <name!r>') or not alphanumeric via .isalnum() ('not alphanumeric: <name!r>'); otherwise returns '<name> registered'. Process the candidate list with try/except, printing successes and 'REJECTED - <message>' for failures.

Expected output
ada99 registered
REJECTED - too short: 'x'
REJECTED - not alphanumeric: 'kai_dev'
mia registered

Write your solution in the editor on the right, then hit Run.

Exercises

Prove it. Reach 80% to complete the lesson.

Mastery Gate0% / 80% required
Easy0/2 solved

A function receives the right type but an unacceptable value (age = -5). What should it raise?

How do you define a custom exception?

Medium0/3 solved

Inside an except block you've logged the error and want the SAME exception to continue propagating. What do you write?

ScenarioA library function catches an internal IndexError during parsing and does: raise ParseError('parse failed'). Users complain that when it fails, they can't tell WHERE in their 10MB file things went wrong — the traceback starts at the raise line.

What two changes fix the debuggability?

Write celsius_to_fahrenheit(c) that raises ValueError(f'below absolute zero: {c}') when c < -273.15, else returns round(c * 9/5 + 32, 1). Call it with 25 (print result), then with -300 inside try/except printing 'error: <message>'. Expected: 77.0 error: below absolute zero: -300

Guard raises with value:The message includes the offending input
Happy path clean:Valid input computes without touching except
Hard0/2 solved

Why should data validation use raise rather than assert?

ScenarioYour team's loader defines JobError(Exception) with subclasses RetryableError and FatalError. A new hire writes: except JobError: retry(). In the first week, a job retried a corrupt-schema failure 400 times.

What's the fix, and the principle?

Complete 80% more exercises to unlock.
Interview Prep

How this shows up in real interviews.

How do you choose what exception type to raise, and why never bare Exception?

Show model answer

Choose by the question the CALLER will ask when catching. Wrong kind of argument → TypeError; right kind, unacceptable value → ValueError; something absent → KeyError/LookupError family; object in the wrong state despite valid inputs → RuntimeError (sklearn's NotFittedError is a domain subclass of exactly this); subclass-must-override → NotImplementedError. When callers need to route on categories no built-in expresses — retryable vs fatal, schema vs connectivity — define custom classes under one domain base. Raising bare Exception breaks the routing entirely: the only way to catch it is `except Exception`, which also sweeps in every bug (NameError, TypeError from typos) — so raising vaguely FORCES callers to catch dangerously. The type is API: callers write except clauses against it, so it deserves the same design care as a return type — and the same stability across versions.

What is exception chaining (`raise X from e`), and what problem does it solve?

Show model answer

It solves the translation-vs-evidence dilemma. Good layering means low-level exceptions (KeyError on 'port', IndexError at byte 40,000) get translated at boundaries into domain terms callers understand (ConfigError('missing db.port'), ParseError('record 48213 malformed')) — otherwise every caller must know your internals to interpret failures. But naive translation (catch KeyError, raise ConfigError) DESTROYS the original traceback — the actual failing line vanishes, and debugging starts from zero. `raise ConfigError(...) from e` does both: callers catch the clean domain type, while the original exception rides along as __cause__, and an unhandled chain prints both tracebacks joined by 'The above exception was the direct cause of the following exception'. The related forms: bare `raise` inside except re-throws the SAME exception untouched (log-and-propagate); and even without `from`, an exception raised inside a handler auto-chains as __context__ ('During handling... another exception occurred') — Python refuses to lose evidence quietly.

Explain fail-fast. Why is raising early better than returning error codes or None?

Show model answer

Fail-fast means detecting bad input or state at the earliest boundary and stopping immediately — guard clauses that raise at the top of the function — so the failure surfaces AT its cause, not three modules downstream where a corrupted value finally explodes. The traceback then points at the actual contract violation with the offending value in the message, turning a multi-hour hunt into a one-minute read. Versus error returns (None, -1, status codes): exceptions can't be silently ignored — an unchecked error return propagates wrong data with no trace, while an uncaught exception stops the world and names itself; they carry structured information (type, message, chained cause) rather than a bare sentinel; and they separate the happy path from error plumbing instead of forcing if-error checks after every call. The place value-or-None remains right is when absence is a NORMAL outcome, not a failure — .get() on an optional key, a search with no match. The rule: expected absence → None by contract; violated contract → raise, immediately, with the evidence attached.

Common Mistakes to Avoid

1) raise Exception('...') — untypeable for callers; pick or define a real type. 2) Messages without the offending value — 'invalid input' helps no one; use {value!r}. 3) Catch-and-re-wrap WITHOUT `from e` — traceback evidence destroyed. 4) assert for input validation — stripped by -O; use raise. 5) Swallowing the original and raising at the wrong level — translate at boundaries, chain always. 6) Specific except AFTER the base class in the same try — unreachable; order narrow-to-broad. 7) Secrets in messages — tracebacks land in logs.

Ask the AI Tutor

Try these prompts in the AI Tutor panel: • 'Quiz me: which type should each of these ten failures raise?' • 'Critique my error messages against the value/rule/culprit formula.' • 'Walk me through designing a hierarchy for a scraper: what's retryable?' • 'Show raise-from vs bare raise vs no chaining, with tracebacks.' • 'Interview mode: ask me exceptions vs error codes and grade my answer.'

Glossary

raise — throw an exception deliberately. Guard clause — a validation-raise at the top of a function. Fail-fast — stop at the boundary where bad state enters. Custom exception — a class inheriting Exception, naming a domain failure. Exception hierarchy — a base class with subclasses encoding categories (retryable/fatal). Bare raise — re-throw the current exception unchanged. raise X from e — throw a translation, chaining the cause. __cause__ — the chained original on a from-raised exception. Error contract — the documented set of types a function may raise. assert — a strippable development check; never input validation. {value!r} — repr in f-strings; makes '' and None visible in messages.

Recommended Resources

• Docs: 'Raising Exceptions' and 'User-defined Exceptions' in the Errors tutorial chapter; skim the built-in exception hierarchy diagram. • Read: Stripe's API error documentation — a production exception taxonomy worth imitating. • Practice: add guard clauses with proper types and value-bearing messages to three functions from earlier modules, then write the try/except a caller would want. • Next in DSM: exceptions guard your logic — now aim them at the outside world: Reading & Writing Files brings open(), with, encodings, and the FileNotFoundError you can finally handle like a professional.

Recap

✓ raise Type(f'rule, got {value!r}') — type for routing, message for humans. ✓ TypeError = wrong kind; ValueError = bad value; RuntimeError = bad state; never bare Exception. ✓ Fail fast: guard clauses at boundaries; everything after runs validated. ✓ Custom classes (one base + subclasses) let callers catch your failures precisely — bugs still crash. ✓ Bare raise = propagate unchanged; raise X from e = translate with evidence attached. ✓ assert documents the impossible in dev; raise enforces contracts in production. Next up: Reading & Writing Files. Your error toolkit meets its biggest customer: the filesystem — open(), the with statement, text vs binary, encodings, and failures you can now handle with precision.

scratchpad — preview this lesson's challenge anytime
username_gate.pyPython
Ready
Output

Run your code to see the output here.