Python · Intermediate
Exceptions and Error Handling in Python
In short: An exception is an object Python raises when something goes wrong; it unwinds the call stack until a matching except clause catches it. try/except handles expected failures such as bad input or a missing file, raise signals a problem from your own code, and finally runs cleanup whether or not an error occurred.
How exceptions work and how to handle them
When Python cannot continue, it raises an exception: dividing by zero, converting "eight" to an integer, reading a missing key. The exception travels up through the calling functions until something catches it; if nothing does, the program stops with a traceback. Exceptions are how Python reports failure, and handling them is how a program turns a crash into a message, a retry or a fallback.
A try block wraps the code that might fail; an except clause names the exception type it handles and receives the exception object with as err. Name the specific exception: except ValueError catches a bad conversion and nothing else, so a genuine bug elsewhere in the block still surfaces with its traceback.
Two optional clauses complete the statement. else runs only if the try block raised nothing, which keeps the success path out of the protected region so its own errors are not mistaken for the one you expected. finally runs no matter what, whether the block succeeded, raised or returned, which makes it the place for cleanup such as closing a connection. Several except clauses can follow one try; the first whose type matches wins, and a tuple such as except (TypeError, ValueError) catches either.
Exceptions form a class hierarchy. ValueError, KeyError, ZeroDivisionError and FileNotFoundError all descend from Exception, and an except clause matches an exception of the named class or any subclass. LookupError therefore catches both KeyError and IndexError, and OSError catches every file and network failure.
Your own code raises with raise SomeError("message"). Use the built-in type that fits (ValueError for a bad argument, TypeError for a wrong kind) or define one by subclassing Exception, which lets callers catch your code's failures precisely. Inside an except block, a bare raise re-raises the current exception after you have logged it, and raise NewError(...) from err records the original as the cause.
Python's style is to try the operation and handle the failure rather than test every precondition first. Opening a file and catching FileNotFoundError is shorter and safer than checking existence beforehand, because the file can vanish in between. That said, do not use exceptions where an ordinary check reads better: if key in d is clearer than catching KeyError for a routine lookup.
Syntax
try:
risky()
except ValueError as err: # one type, with the exception object
print("bad value:", err)
except (TypeError, KeyError): # several types in one clause
...
else: # only if no exception was raised
...
finally: # always, even after return or an unhandled error
...
raise ValueError("amount must be positive")
class InsufficientStock(Exception):
passOrder the except clauses from most specific to most general; a clause for Exception placed first would catch everything and hide the later ones.
Catching a ValueError from bad input
Quantities are read one per line. Lines that are not whole numbers are reported and skipped instead of stopping the program.
count = int(input())
total = 0
skipped = 0
for _ in range(count):
text = input().strip()
try:
total += int(text)
except ValueError:
print(f"skipping {text!r}: not a whole number")
skipped += 1
print("total:", total)
print("skipped:", skipped)Input given to the program: 5 ↵ 12 ↵ eight ↵ 7.5 ↵ 20 ↵ 3
Output
skipping 'eight': not a whole number skipping '7.5': not a whole number total: 35 skipped: 2
int("eight") and int("7.5") both raise ValueError, which the except clause turns into a message. Because the try block contains only the conversion, the handler cannot accidentally catch an unrelated error from elsewhere in the loop. The !r in the f-string prints the offending text with quotes so a stray space would be visible.
Multiple except clauses, else and finally
One function called three times: a success, a division by zero and a wrong type. Watch which clauses run in each case.
def share(amount, people):
try:
each = amount / people
except ZeroDivisionError:
print("nobody to share with")
except TypeError as err:
print("bad input:", err)
else:
print(f"each gets {each:.2f}")
finally:
print("done")
share(90, 4)
share(90, 0)
share("90", 4)Output
each gets 22.50 done nobody to share with done bad input: unsupported operand type(s) for /: 'str' and 'int' done
The first call raises nothing, so else prints the result. The second raises ZeroDivisionError, matched by the first clause; the third raises TypeError, matched by the second, and as err gives access to Python's own message. finally prints done all three times. Putting the result line in else rather than inside try means a formatting bug there would not be mistaken for a division error.
Raising built-in and custom exceptions
A stock-taking function raises KeyError for unknown items and a custom InsufficientStock for shortages; the caller handles each differently.
class InsufficientStock(Exception):
pass
def take(stock, item, qty):
if item not in stock:
raise KeyError(item)
if stock[item] < qty:
raise InsufficientStock(f"only {stock[item]} {item} left, wanted {qty}")
stock[item] -= qty
stock = {"paint": 5, "brush": 2}
requests = [("paint", 3), ("brush", 4), ("tape", 1), ("paint", 2)]
for item, qty in requests:
try:
take(stock, item, qty)
print(f"took {qty} {item}")
except InsufficientStock as err:
print("refused:", err)
except KeyError as err:
print("unknown item:", err)
print(stock)Output
took 3 paint
refused: only 2 brush left, wanted 4
unknown item: 'tape'
took 2 paint
{'paint': 0, 'brush': 2}The custom class needs no body: inheriting from Exception gives it everything, including the message passed to the constructor, which print shows through str(err). KeyError prints its argument in quotes, which is how Python formats that particular exception. Because each request is handled inside the loop, one refusal does not stop the others, and the final stock reflects only the successful requests.
Common built-in exceptions
| Exception | Raised when | Typical trigger |
|---|---|---|
| ValueError | a value has the right type but unusable content | int("abc") |
| TypeError | an operation receives the wrong type | "a" + 1 |
| KeyError | a dictionary key is missing | d["absent"] |
| IndexError | a sequence index is out of range | [1, 2][5] |
| ZeroDivisionError | division or modulo by zero | x / 0 |
| FileNotFoundError | a file to read does not exist | open("missing.txt") |
| AttributeError | an object has no such attribute or method | None.upper() |
Common mistakes
Using a bare except:
Why it goes wrong: It catches everything, including
KeyboardInterrupt,SystemExitand the bugs you did not know about, so the program hides its own errors and cannot be stopped cleanly.Fix: Catch the specific type you expect, or at the very least
except Exception as err:and reporterr.Wrapping too much code in one try
Why it goes wrong: If the
tryblock has twenty lines, anexcept ValueErrormay catch aValueErrorfrom a line you never meant to protect, and the handler's message will be wrong.Fix: Keep the
tryblock to the one or two statements that can fail; put the follow-up code inelse.Python · fixtry: qty = int(text) except ValueError: qty = 0 else: process(qty)Catching an exception and doing nothing
Why it goes wrong:
except ValueError: passmakes the failure invisible. The program continues with wrong data and the real problem shows up somewhere far away.Fix: Handle it meaningfully: report it, substitute a default, or re-raise with a bare
raise.Raising a generic Exception for everything
Why it goes wrong:
raise Exception("bad input")forces every caller to catchException, which also catches unrelated bugs, so nobody can handle your error precisely.Fix: Raise the matching built-in (
ValueError,TypeError) or a small custom class derived fromException.
Where you use this
Input validation is the everyday case. A program that reads numbers, dates or file names from a user, a form or a file cannot assume they are well formed; wrapping each conversion in try/except ValueError lets it report the bad line and carry on instead of dying on the first typo. The same shape protects network calls and file access, where the failure is outside your control.
Custom exceptions earn their place in code other people call. A payment module that raises InsufficientFunds and CardDeclined lets the caller handle those two cases differently while letting an unexpected TypeError propagate as the bug it is. The finally clause keeps resources tidy: a temporary file removed, a lock released, a progress message printed, whether the work succeeded or not.
try:
with open(path, encoding="utf-8") as f:
data = f.read()
except FileNotFoundError:
data = "" # first run: nothing saved yetKey points
- Exceptions are objects raised on failure; an uncaught one stops the program with a traceback.
- Catch specific types such as
except ValueError, never a bareexcept:. elseruns only on success;finallyalways runs, so it is the place for cleanup.- An
exceptclause matches the named class and all its subclasses;Exceptionis the base of the ordinary ones. - Raise built-in types for ordinary misuse and small custom subclasses for errors specific to your code.
- Keep the
tryblock small and never silence an exception without a reason.
Try it yourself
The program converts the line it reads to an integer and prints double the value. Add a try/except so that a line that is not a whole number prints not a number instead of crashing.
text = input().strip()
value = int(text)
print(value * 2)fortynot a numbertext = input().strip()
try:
value = int(text)
except ValueError:
print("not a number")
else:
print(value * 2)Practise this
Related lessons
- Reading and Writing Files in PythonHow to read and write text files in Python with open and with: file modes, reading line by line, appending, explicit encodings, and pathlib for paths.9 min
- Functions in PythonHow to define and call Python functions with def, pass arguments, return results, use default and keyword arguments, and why variables inside are local.9 min
- Conditions: if, elif and else in PythonHow Python chooses between paths with if, elif and else, how truthiness works, why branch order matters, and how to write conditions that stay readable.8 min
- Inheritance in PythonHow Python inheritance lets a subclass reuse and extend a parent class: super().__init__, overriding, isinstance, the MRO and when composition is better.10 min
Frequently asked questions
What is the difference between except Exception and a bare except?
A bare except: catches every exception including KeyboardInterrupt and SystemExit, which derive from BaseException rather than Exception, so it can stop a program from exiting when the user presses Ctrl+C. except Exception leaves those alone but still catches every ordinary error. Both are too broad for most handlers; name the specific type when you can.
When does the finally block run?
Always: after the try block finishes normally, after an except clause handles an exception, when an exception is not handled and continues upward, and even when the try block executes return, break or continue. That guarantee is why cleanup code belongs there.
Should I check a condition first or catch the exception?
Catch the exception when the check would duplicate the operation or could become stale, as with file existence and network availability, or when the failure is rare. Use a plain check when it reads better and is cheap, such as if key in d before a lookup. Both are idiomatic; pick the one that makes the code clearest.
How this page was checked. Every program on it was run with CPython 3.11 at build time; runs in your browser on CPython 3.14 (Pyodide) by the publishing checks, and the output shown is what it printed. The Run buttons execute the same code on your device; nothing you type is sent anywhere.