Catching exceptions is only half of error handling. The other half is signaling that something has gone wrong in your own code, and that's what raise is for. This lesson covers how to raise built-in exceptions like ValueError and TypeError, how to pick the right one, how to re-raise inside an except block, and when raising an exception is the wrong choice in the first place.
raise Statementraise is the keyword that interrupts normal execution and starts looking for a matching except block up the call stack. You hand it an exception instance (or an exception class) and Python takes over from there.
The first call returns 40.0, the discounted price. The second call hits the raise, and execution stops cold. Python prints the traceback and the program exits, because nothing along the way caught the exception.
The traceback reads from outermost to innermost. The bottom line tells you the exception type and message; the lines above tell you how Python got there. Building this habit early is worth a lot when you're debugging real code.
You don't always have to let the exception kill the program. If a caller is prepared to deal with the failure, they can wrap the call in a try/except block. That's the whole point of raising rather than printing: you give the caller a chance to recover.
Wait, the message says "150" instead of the full sentence. That's because print(e) only shows the exception's arguments, and when you format the message with e, you get the same thing. The traceback shown earlier includes the class name automatically, but plain print(e) does not. If you want the class name too, format it explicitly: print(f"{type(e).__name__}: {e}").
Python ships with a long list of built-in exception classes, and most of the time you should reach for one of those rather than invent something new. Picking the right one helps callers handle errors correctly, because they can write except ValueError and trust that you'll only raise ValueError when a value is bad.
The five you'll use most when raising your own errors are:
| Exception | When to Raise It | Example Trigger |
|---|---|---|
ValueError | Right type, wrong value | Negative price, discount of 150% |
TypeError | Wrong type entirely | Passing a string where a number is expected |
KeyError | Asked for a dictionary key that doesn't exist | Cart has no item with id "sku-99" |
IndexError | Asked for a sequence index out of range | Reading position 5 of a 3-item cart |
RuntimeError | Something went wrong and none of the above fits | A pricing service returned an inconsistent state |
A ValueError is the most common one to raise from your own code. The argument's type is fine, but the value itself doesn't make sense for what you're doing.
-1 is a valid integer, so the type is fine. It's just not a value that makes sense for a quantity. That's exactly the case ValueError is for.
TypeError is for when the argument is the wrong type altogether, not just a bad value:
The string "two" isn't a number at all, so the issue is the type, not the value. Python's own built-ins behave the same way: len(5) raises TypeError because 5 isn't something you can take the length of, while int("abc") raises ValueError because "abc" is a string but not a string that represents an integer.
KeyError and IndexError are the ones you usually catch rather than raise, because Python raises them for you when you index into a dict or list incorrectly. You'd raise them yourself only if you're writing a class that behaves like a dict or a list and want callers to handle missing lookups the same way they would for the built-ins:
RuntimeError is the fallback for the rare case where none of the more specific exceptions fits. Don't reach for it first. If you find yourself raising RuntimeError a lot, the right move is usually to define a custom exception so callers can catch your specific failure mode without catching every other RuntimeError in the program.
A vague exception message wastes everybody's time. "Invalid input" tells you nothing about what was invalid or what would have been valid. Three principles produce better messages.
Say what was wrong. State the specific thing that failed the check, not a generic "bad data" line.
Include the offending value. If the value can be printed safely, include it. Future-you will appreciate seeing the actual data that triggered the error.
Hint at what would have worked. Give the caller a clue about the expected range, type, or format.
Compare a weak message:
with a better one:
If the bad call is apply_discount(50, 150), the second version tells you exactly what came in (150) and what was expected (between 0 and 100). You don't have to dig into the traceback, find the call site, and inspect variables. The message does the work for you.
Use the !r flag in f-strings when you're showing a value that might be a string or None. It calls repr() on the value, which puts quotes around strings and uses None (not blank) for None. That distinction matters when the value is empty:
The '' makes it clear that the input was an empty string, not the word "empty" or some other oddity. Without !r, the message would have read got , which is worse than useless.
raise SomeError(...) is the form you'll write most often, but Python actually accepts three different operands.
Pass an instance (the standard form). This is what every previous example has used: raise ValueError("message"). You create the exception with your message and Python raises it.
Pass a class with no arguments. Python instantiates the class for you with no arguments. The exception has no message. This works, but it usually isn't what you want, because callers and tracebacks lose useful information:
Output:
No clue what went wrong. The empty message is fine for very generic "I just need to abort" situations, but for almost everything else, write the message.
Pass an existing exception instance. You can build the exception ahead of time and pass it as an object. This is useful when the exception is captured in one place and raised in another, but it's uncommon in everyday code:
Behaviorally, raise ValueError("price must be positive") and the two-line version are identical. The class-without-arguments form is the only one that meaningfully differs, because it loses the message.
Cost: Creating an exception object is cheap on its own, but raising it forces Python to walk the stack, capture frame info, and look for a handler. The total cost is much higher than a plain return. Raise exceptions for exceptional situations, not for routine signaling in a hot loop.
raiseSometimes you want to catch an exception, do something with it (log it, update a counter, clean up state), and then let it keep going so the caller can decide what to do. A bare raise inside an except block re-raises the exception that's currently being handled:
process_order caught the ValueError, printed its log line, and then re-raised. Whatever called process_order still sees the original exception, with its original message and original traceback pointing all the way back to charge_customer. That last part matters: if you instead wrote raise ValueError(...) inside the except block, the traceback would point to the re-raise location, and the original cause would be harder to find.
The pattern looks like this:
The middle layer (process_order) is doing exactly two jobs: recording that the failure happened, and stepping aside so the caller can decide whether to retry, surface the error to the user, or give up. It doesn't pretend it can fix the problem on its own.
A bare raise only works inside an except block. Outside an except, there's no exception to re-raise, and Python will give you a RuntimeError:
Output:
The catch-log-reraise pattern shows up in real code more than any other use of raise. Three reasons.
Visibility. You want a record that the failure happened at this layer, even if a higher layer recovers. Logging it where you catch it preserves the context (which order, which customer, which retry attempt) that's natural at this level but harder to reconstruct higher up.
Cleanup. You may have allocated resources or partial state that needs to be undone even if you can't actually handle the error. Cleanup, then re-raise, is the standard pattern. A finally block handles many of these cases without the catch, but when cleanup depends on knowing what went wrong, you catch.
Adding context without hiding the cause. You might want to attach a more specific exception to it, like turning a low-level KeyError into a higher-level error that says "no such product." That falls under exception chaining. For now, just remember that re-raising preserves the original failure.
What you should not do is catch an exception and silently swallow it. That hides bugs and makes debugging miserable for whoever inherits the code later.
Python has a strong opinion about how to deal with operations that might fail. The Python style is EAFP: Easier to Ask Forgiveness than Permission. You attempt the operation and catch any exception that comes out. Many other languages favor LBYL: Look Before You Leap. You check every precondition first and only do the operation if all the checks pass.
Here's the same lookup written both ways. The cart is a dictionary keyed by product id:
The EAFP version reads cart[item_id]["price"] directly. If the key is missing, the KeyError is caught. The LBYL version checks item_id in cart first. Both work. So why does Python lean toward EAFP?
The LBYL version does the lookup twice: once for the in check, once for the actual access. The EAFP version does it once. More important, LBYL has a subtle bug class: between the check and the action, the data might change (in concurrent code) and what you checked is no longer true. EAFP doesn't have that gap because there's no separate check.
EAFP also handles more cases cleanly. Suppose the cart values come from a list of dicts and some entries are missing the price key. With LBYL you'd write if item_id in cart and "price" in cart[item_id]. With EAFP, the same try block catches both kinds of KeyError at once.
Both paths reach the same destination, but the EAFP path avoids the double lookup and the race-condition window.
When does LBYL still make sense? Mostly when the failure case is the common case rather than the exception. If you expect most lookups to miss, the try block runs into the exception machinery every time, which is more expensive than a simple membership check. The same goes for hot loops where most iterations would raise: paying the cost of raising on every iteration is wasteful.
Cost: Raising and handling an exception is more expensive than checking a condition with if. The cost only matters when failures are common and code runs in a tight loop. For everyday flow, EAFP is cleaner and the cost difference is negligible.
So the rule of thumb most Python developers follow: use EAFP by default, switch to LBYL only when the "look before you leap" check is cheap and the failure case is common. Plain dict.get(key, default) is also an option that sidesteps both styles when you're just looking up a value with a fallback.
raise is the right tool for exceptional situations. It's the wrong tool for normal program flow. Three patterns are common enough to call out.
Don't use exceptions to terminate loops. Old code sometimes catches StopIteration to break out of a loop. Python iterators do raise StopIteration, but that's an internal protocol. In your own code, use break:
The second version is shorter, faster, and easier to read. There's no exception flying around. break exists for exactly this purpose.
Don't use exceptions for routine return values. If a "not found" result is expected, returning None (or a default) is often cleaner than raising. Compare:
If "not found" is something the caller is going to check for on every call, raising forces them to wrap every call in try/except, which is heavy. Returning None keeps the caller's code clean.
Opinions on this one are genuinely split. Some developers prefer raising in this scenario because it makes the "not found" case impossible to forget, whereas None can slip through and cause a NoneType error far away from the source. Both views have merit. The general guideline: raise when the failure means the program can't reasonably continue without the value, return None (or a default) when the caller is expected to handle missing data routinely.
Don't use exceptions for validation that has a natural return value. If you're writing a is_valid_email(email) function, it should return True or False, not raise on invalid input. Save raising for the function that actually tries to send the email.
When raise and except show up in real code, two patterns come up often enough that they deserve their own warning.
Smell #1: catching too broadly. A bare except: (no exception type) catches everything, including KeyboardInterrupt and SystemExit. You lose the ability to stop your program with Ctrl-C and you hide every bug behind a single handler. Just slightly better is except Exception:, which at least leaves KeyboardInterrupt alone, but it still catches TypeError, AttributeError, and every other bug that's nothing to do with what you meant to handle.
The narrow version reports the specific kind of failure and lets unrelated bugs (a misspelled function name, a None slipping through) surface where you can see them.
Smell #2: catching and ignoring. This is the worst of the two, and a close cousin of the first. The exception is caught and then nothing happens:
If you genuinely want to ignore the error, write a comment explaining why. Most of the time, "ignore" is the wrong call: at minimum, log the failure so it shows up in your logs and you find out about real problems instead of having them disappear silently. Hidden bugs are the worst kind because there's no signal that anything went wrong.
There's a related anti-pattern where the except block prints the error but does nothing else, then execution continues as if the call had succeeded. That can be even more confusing, because the program proceeds with garbage state. If the call failed, either re-raise or take a clear corrective action; don't pretend it succeeded.
10 quizzes