Beyond a bare try/except
Error handling is where production code differs most from tutorial code: user input is malformed, files go missing, networks drop mid-request. The difference between a service that reports a clear error and one that silently corrupts data usually comes down to a few well-placed except clauses.
You met try/except in Python for Beginners and used it in lessons 4-1 and 5-2. Now the full toolkit:
try: data = json.loads(text) except json.JSONDecodeError as e: print("bad input:", e) else: print("parsed", len(data), "keys") finally: print("attempt finished")
- Catch specific exceptions. A bare
except:swallows typos likeNameErrorand hides real bugs. as ebinds the exception object so you can log its message.elseruns only when no exception happened, keeping the happy path out of the risky block.finallyalways runs, for cleanup.
And raise ValueError("amount must be positive") throws your own error when a caller hands you nonsense. Failing loudly beats corrupting data silently.
try, except, else, and finally together
One function handles good input and bad JSON, and reports the attempt either way through finally.
import json def parse(text): try: data = json.loads(text) except json.JSONDecodeError: print("bad input") else: print("keys:", sorted(data.keys())) finally: print("done") parse('{"a": 1, "b": 2}') parse('not json at all')
Output
keys: ['a', 'b'] done bad input done
Each clause has one job. The try block holds only the risky call. except json.JSONDecodeError names the specific failure being handled, rather than catching everything. else holds the work that only makes sense when the parse succeeded. finally runs on both paths, which is why done appears twice.
Keeping sorted(data.keys()) in the else rather than inside the try is deliberate. If it lived in the try and raised its own error, the except clause would catch it and mislabel a bug as bad input.
Custom exceptions
Big programs define their own exception types so callers can react precisely. It is lesson 3-4 inheritance, usually with an empty body:
class InsufficientFunds(Exception): pass def withdraw(balance, amount): if amount > balance: raise InsufficientFunds(f"need {amount}, have {balance}") return balance - amount
Now except InsufficientFunds: catches exactly this business error while a TypeError from a genuine bug still crashes visibly, which is what you want. Convention: name them like errors, inherit from Exception (never from BaseException), and put them near the code that raises them.
EmptyCartError
A custom exception class gives a specific failure its own name. EmptyCartError subclasses Exception with an empty body, checkout raises it when there is nothing to buy, and the caller catches that exact type.
class EmptyCartError(Exception): pass def checkout(cart): if not cart: raise EmptyCartError("nothing to buy") return sum(cart) for cart in [[12, 30], []]: try: print("total:", checkout(cart)) except EmptyCartError as e: print("error:", e)
Output
total: 42
error: nothing to buyclass EmptyCartError(Exception): pass is the entire definition. Subclassing Exception is all it takes for raise and except to work with your new type.
Inside the function, if not cart: detects the empty list because empty containers are falsy, an idea lesson 9-1 returns to. The message passed to the constructor is what print shows when the caught exception e is printed, so writing a message a human can act on is worth the few extra characters.
A named exception type lets callers respond to this problem specifically. Raising a bare
ValueErrorinstead would force every caller to inspect the message text to figure out what went wrong.
Why a bare except is harmful
A bare except: catches everything, including the bugs in your own code, and that is what makes it dangerous.
Suppose you misspell a variable name inside the try block. That raises NameError, a bare except swallows it silently, and the program continues along the error path as though the operation had merely failed. The real defect is now invisible, and you get to hunt for it later with no traceback to help.
The discipline is to catch the narrowest exception you genuinely expect and have a plan for. except json.JSONDecodeError says something true about the situation. except: says nothing at all.
What the else clause is for
In a try / except / else statement, the else block runs only when the try block finished without raising anything.
else is the no-exception branch. It runs after the try body completes cleanly, and it is skipped entirely whenever an except clause fires.
The reason to use it is precision. Moving the happy-path work into else keeps the try block down to just the line that might fail, so you can never accidentally catch an exception raised by your own follow-up code and misreport what went wrong.
| Clause | Runs when |
|---|---|
try | always, first |
except | a matching exception was raised |
else | no exception was raised |
finally | always, on every exit path |