Python try except: Error Handling Explained with Examples
In Python, you handle an error by putting the risky code inside a try block and the recovery code inside an except block that names the exception you expect. If the try block raises that exception, Python jumps to the except block instead of crashing:
raw = "twenty"
try:
age = int(raw)
except ValueError:
print(f"{raw!r} is not a whole number")
# 'twenty' is not a whole number
That is the core pattern. This guide covers the rest of the statement (else and finally), which exceptions to catch, how to raise your own, raise ... from ..., context managers, and the mistakes that trip up beginners in assignments and coding rounds.
The full flow: try, except, else, finally
A try statement can have four parts. Each one runs at a specific time:
try: the code that might fail. Keep it short.except: runs only if thetryblock raised a matching exception.else: runs only if thetryblock finished without any exception.finally: runs every time, whether there was an error or not, even if areturnhappened earlier.
def divide(a, b):
try:
result = a / b
except ZeroDivisionError:
print("except: cannot divide by zero")
return None
else:
print("else: no exception")
return result
finally:
print("finally: always runs")
print(divide(10, 2))
print(divide(10, 0))
Output:
else: no exception
finally: always runs
5.0
except: cannot divide by zero
finally: always runs
None
Notice that finally ran in both calls, and it ran before the function actually handed back its return value. That is what makes finally the right place for cleanup such as closing a connection.
Why use else instead of the end of try? Anything inside try is protected by the except. If the success-path code also raised a ZeroDivisionError by accident, it would be caught and reported with the wrong message. else keeps the protected region as small as possible.
Catch specific exceptions, never a bare except
Always name the exception you expect. You can catch several at once with a tuple, and use as to get the exception object:
def parse_price(text):
try:
return float(text.replace("₹", "").replace(",", ""))
except (ValueError, AttributeError) as exc:
print(f"could not parse {text!r}: {type(exc).__name__}")
return None
print(parse_price("₹1,299.50"))
print(parse_price("free"))
print(parse_price(None))
Output:
1299.5
could not parse 'free': ValueError
None
could not parse None: AttributeError
None
A bare except: catches everything, including KeyboardInterrupt (Ctrl+C) and SystemExit, and it hides real bugs. Here is what not to do:
try:
totl = 100
print(total) # typo: should be totl
except:
print("Something went wrong")
# Something went wrong
The real problem is a NameError caused by a typo, but the bare except turns it into a vague message. Without the try, Python would have shown the exact line and the misspelt name. except Exception: is slightly better because it lets Ctrl+C through, but it has the same bug-hiding problem. Use it only at the very top of a program, where you log the full error and exit.
Exception hierarchy basics
Exceptions are classes, and they form a tree. An except clause catches the named class and every class below it.
BaseException
+-- SystemExit
+-- KeyboardInterrupt
+-- Exception
+-- ArithmeticError
| +-- ZeroDivisionError
+-- LookupError
| +-- IndexError
| +-- KeyError
+-- OSError
| +-- FileNotFoundError
+-- TypeError
+-- ValueError
This is a simplified slice. Check any relationship yourself:
print(issubclass(ZeroDivisionError, ArithmeticError)) # True
print(issubclass(KeyError, LookupError)) # True
print(issubclass(KeyboardInterrupt, Exception)) # False
print(FileNotFoundError.__mro__)
# (<class 'FileNotFoundError'>, <class 'OSError'>, <class 'Exception'>, <class 'BaseException'>, <class 'object'>)
Python checks except clauses from top to bottom and runs the first one that matches. So put specific exceptions before general ones:
data = {"name": "Meera"}
try:
data["email"]
except KeyError:
print("KeyError handler")
except LookupError:
print("LookupError handler")
# KeyError handler
If you swapped the two clauses, the LookupError handler would catch the KeyError and the specific handler would never run. Python does not warn you about this, so the order is your job.
Raising and re-raising exceptions
Use raise to signal that a function received something it cannot work with. Pick the built-in exception that fits: TypeError for the wrong type, ValueError for the right type with a bad value.
def set_age(age):
if not isinstance(age, int):
raise TypeError(f"age must be int, got {type(age).__name__}")
if age < 0:
raise ValueError("age cannot be negative")
return age
try:
set_age(-3)
except ValueError as exc:
print("Rejected:", exc)
# Rejected: age cannot be negative
Sometimes you want to react to an error (log it, say) but still let the caller deal with it. A bare raise inside an except block re-raises the same exception with its original traceback:
def load_config(path):
try:
with open(path) as f:
return f.read()
except FileNotFoundError:
print(f"log: missing config {path}")
raise
try:
load_config("missing.toml")
except FileNotFoundError as exc:
print("Caller saw:", exc)
# log: missing config missing.toml
# Caller saw: [Errno 2] No such file or directory: 'missing.toml'
Custom exceptions
For your own code, define exception classes by inheriting from Exception. A small base class for your module lets callers catch all of your errors with one clause, and subclasses can carry extra data.
class PaymentError(Exception):
"""Base class for payment failures."""
class InsufficientBalanceError(PaymentError):
def __init__(self, balance, amount):
self.balance = balance
self.amount = amount
super().__init__(f"need {amount}, have {balance}")
def pay(balance, amount):
if amount > balance:
raise InsufficientBalanceError(balance, amount)
return balance - amount
try:
pay(500, 1200)
except PaymentError as exc:
print(type(exc).__name__, "-", exc)
print("short by", exc.amount - exc.balance)
# InsufficientBalanceError - need 1200, have 500
# short by 700
Name custom exceptions with an Error suffix, as the standard library does, and inherit from Exception, not BaseException.
raise ... from ...: keep the original cause
When you catch a low-level error and raise a clearer one, use raise NewError(...) from exc. The new exception stores the original in __cause__, and the traceback shows both, joined by the line "The above exception was the direct cause of the following exception".
class ConfigError(Exception):
pass
def read_port(settings):
try:
return int(settings["port"])
except (KeyError, ValueError) as exc:
raise ConfigError("port setting is missing or invalid") from exc
try:
read_port({"port": "eighty"})
except ConfigError as exc:
print(exc)
print("caused by:", repr(exc.__cause__))
# port setting is missing or invalid
# caused by: ValueError("invalid literal for int() with base 10: 'eighty'")
If you raise inside an except block without from, Python still links the two errors, but the traceback says "During handling of the above exception, another exception occurred", which reads like a second bug. from exc states that the translation was intentional. raise ... from None hides the original when it adds nothing useful.
Context managers handle cleanup for you
A with statement guarantees cleanup even when an exception is raised. with open(path) as f: closes the file whether the block succeeds or fails, which is why you rarely need to write finally: f.close() yourself.
You can write your own context manager with contextlib.contextmanager. The code after yield sits in a finally block, so it runs on success and on error:
from contextlib import contextmanager
@contextmanager
def step(name):
print(f"start {name}")
try:
yield
finally:
print(f"end {name}")
try:
with step("import marks"):
int("A+")
except ValueError as exc:
print("handled:", exc)
# start import marks
# end import marks
# handled: invalid literal for int() with base 10: 'A+'
For the common case of "ignore this one error", contextlib.suppress is shorter and clearer than an empty except:
import os
from contextlib import suppress
with suppress(FileNotFoundError):
os.remove("temp_report.csv")
print("done")
# done
The context managers lesson shows how to write one as a class with __enter__ and __exit__.
EAFP vs LBYL
There are two styles for dealing with things that might fail:
- LBYL (look before you leap): check first, then act.
- EAFP (easier to ask forgiveness than permission): just try it, and handle the exception.
stock = {"pen": 12, "notebook": 0}
# LBYL
if "eraser" in stock:
print(stock["eraser"])
else:
print("no eraser entry")
# EAFP
try:
print(stock["eraser"])
except KeyError:
print("no eraser entry")
Both print no eraser entry. Python code usually prefers EAFP. It avoids checking twice, and it is safer when the situation can change between the check and the action: a file that exists when you call os.path.exists() can be deleted before you open it. LBYL is fine when failure is the normal, expected case and the check is cheap. For dictionaries specifically, stock.get("eraser", 0) is simpler than either.
Common beginner mistakes
- Bare
except:orexcept Exception: pass. Errors disappear silently and you debug the wrong thing for an hour. - Too much code inside
try. Wrap only the line that can raise the exception you are handling. Move the rest toelseor after the statement. - Broad exception before specific. The specific handler never runs, and Python will not warn you.
- Returning from
finally. Areturninfinallydiscards any exception that was being raised. Python 3.14 added aSyntaxWarningfor this, but older versions accept it without complaint:
def risky():
try:
raise ValueError("lost forever")
finally:
return "finally wins"
print(risky()) # finally wins
- Carrying on with bad data. Printing a message and returning
Noneoften causes a confusingTypeErrora few lines later. Handle the error properly or let it propagate. - Forgetting the parentheses for multiple exceptions. Write
except (ValueError, TypeError) as exc:. The tuple form works in every Python 3 version, and the parentheses are required when you useas.
Read the traceback before you catch anything
Before you add a try, read the traceback from the bottom: the last line gives the exception type and message, and the lines above show where it happened. Often the right fix is correcting the bug, not catching the exception. The debugging lesson walks through reading tracebacks step by step.
Practise error handling in the browser
The error handling lesson has runnable examples and graded exercises for everything above. Python runs in the browser with no install, so you can also paste any example from this post into the Python terminal, break it on purpose, and watch which handler fires. For more drills, try the practice editor.