Python Exception Handling: try, except, finally and Custom Exceptions Explained
Learn how to catch errors without hiding bugs. This guide covers every part of the try statement, raising your own exceptions, chaining errors and the habits that keep Python programs easy to debug.
- Use
tryandexceptto handle errors you can actually recover from. - Catch specific exception types. A bare
except:hides real bugs. elseruns only when no error occurred, andfinallyalways runs.- Raise custom exceptions for errors in your own domain, and use
fromto keep the original cause.
Every real program meets bad input, missing files and failed network calls. Python exception handling lets you respond to those problems in a controlled way, rather than letting the whole program crash with a stack trace in front of a user.
The syntax takes minutes to learn. The harder skill is knowing what to catch, what to let fail, and how to keep errors useful for whoever debugs them later. We ran every example below on Python 3.13, and the outputs are from those runs.
What is an exception?
An exception is an object Python raises when something goes wrong while code is running. If nothing handles it, the program stops and prints a traceback showing where it happened.
>>> int("abc")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: invalid literal for int() with base 10: 'abc'
Read a traceback from the bottom up. The last line gives you the exception type and message, and the lines above it show the path the code took to get there.
Exceptions differ from syntax errors. A syntax error means Python could not even parse your code, so nothing runs. No try block in that same file can rescue you from one, because you have to fix the code itself.
try and except
Put the risky code in try, and say what to do about a specific failure in except:
try:
age = int(text)
except ValueError:
age = None
Python runs the try block. If int() raises a ValueError, it jumps to the matching handler and then continues after the whole statement. If a different exception occurs, this handler ignores it, and the exception keeps travelling up the call stack until something else handles it.
You can handle different failures differently, and read details from the exception using as:
try:
with open("config.json") as file:
settings = json.load(file)
except FileNotFoundError:
settings = {}
except json.JSONDecodeError as error:
print(f"Bad JSON on line {error.lineno}")
raise
Python checks handlers from top to bottom and runs the first match, so put specific types before general ones. To treat several types the same way, group them in a tuple: except (KeyError, ValueError):.
else and finally
The full statement has four parts. This function uses them all:
def read_age(text):
try:
age = int(text)
except ValueError:
print(f"{text!r} is not a number")
return None
else:
print("Parsed OK")
return age
finally:
print("Finished checking")
print(read_age("42"))
print(read_age("abc"))
# Parsed OK
# Finished checking
# 42
# 'abc' is not a number
# Finished checking
# None
| Block | When it runs | Use it for |
|---|---|---|
try |
Always, first | Only the code that may fail |
except |
When a matching exception occurs | Recovering or reporting |
else |
When try raised nothing |
Follow-up work that should not be guarded |
finally |
Every time, even after return |
Cleanup, such as releasing resources |
Notice in the output that finally ran before the caller printed the returned value. That is the point of it: cleanup happens no matter how the block exits.
Why use else at all? It keeps the try block small. If you put follow-up code inside try, your handler might accidentally catch an unrelated error from that code, and you would blame the wrong line.
Common built-in exceptions
| Exception | Typical cause |
|---|---|
ValueError |
Right type but a bad value, like int("abc") |
TypeError |
Wrong type, like "a" + 1 |
KeyError |
A dictionary key does not exist |
IndexError |
A list position is out of range |
AttributeError |
An object has no such attribute |
FileNotFoundError |
A file path does not exist |
ZeroDivisionError |
Dividing by zero |
ImportError |
A module or name cannot be imported |
All of these inherit from Exception. Above that sits BaseException, which also covers KeyboardInterrupt and SystemExit. Those two are meant to stop your program, which is a major reason a bare except: is dangerous, since it catches them as well.
Raising exceptions
Use raise when your code detects a problem it cannot sensibly deal with itself:
def set_discount(percent):
if not 0 <= percent <= 100:
raise ValueError(f"percent must be 0-100, got {percent}")
return percent
Failing early with a clear message beats returning a strange value that causes confusion ten calls later. Inside a handler, a plain raise with nothing after it re-raises the current exception. That is the right pattern when you want to log a failure and still let it continue to the caller.
Custom exceptions
Built-in types describe general problems. Your application has its own, and a named class lets callers catch exactly those:
class InsufficientFunds(Exception):
def __init__(self, balance, amount):
super().__init__(f"Need {amount}, but balance is {balance}")
self.balance = balance
self.amount = amount
def withdraw(balance, amount):
if amount > balance:
raise InsufficientFunds(balance, amount)
return balance - amount
try:
withdraw(100, 250)
except InsufficientFunds as error:
print(error, "| short by", error.amount - error.balance)
# Need 250, but balance is 100 | short by 150
Because the exception carries data as attributes, the caller does not have to parse a message string to work out the shortfall. For a larger project, create one base class, such as class ShopError(Exception), and inherit from it, so callers can catch everything from your library in one line if they want to.
Exception chaining with from
When you translate a low-level error into a meaningful one, keep the original cause by using raise ... from:
class ConfigError(Exception):
pass
def load_port(settings):
try:
return int(settings["port"])
except (KeyError, ValueError) as error:
raise ConfigError("Invalid port setting") from error
try:
load_port({"port": "abc"})
except ConfigError as error:
print(type(error).__name__, "| cause:", repr(error.__cause__))
# ConfigError | cause: ValueError("invalid literal for int() with base 10: 'abc'")
The caller deals with one clear error, while the traceback still shows the original problem. Without the original cause, whoever debugs this later has to guess what went wrong.
ExceptionGroup and except*
Python 3.11 added a way to raise several errors at once, which is useful when concurrent tasks or batch jobs fail in more than one way. The except* syntax handles each type within the group:
try:
raise ExceptionGroup("batch failed", [ValueError("bad row"), KeyError("id")])
except* ValueError as group:
print("value errors:", group.exceptions)
except* KeyError as group:
print("key errors:", group.exceptions)
# value errors: (ValueError('bad row'),)
# key errors: (KeyError('id'),)
Unlike normal handlers, more than one except* block can run. You will mostly meet this through asyncio.TaskGroup, so beginners can safely come back to it later.
Context managers for cleanup
You could close a file inside finally, but the with statement does that for you, even when an exception occurs:
with open("report.txt") as file:
data = file.read()
Prefer with for files, locks and database connections. Keep finally for cleanup that has no context manager. Handling errors around shared setup and teardown also becomes much neater with Python decorators, such as a retry wrapper.
EAFP vs LBYL
Python programmers often prefer EAFP, “easier to ask forgiveness than permission”. Rather than checking first, you try the operation and handle failure.
# LBYL: look before you leap
if "port" in settings:
port = settings["port"]
# EAFP: just try it
try:
port = settings["port"]
except KeyError:
port = 8080
EAFP avoids race conditions. A file can disappear between checking it exists and opening it, so a check does not protect you. But do not force it. For simple defaults, settings.get("port", 8080) is the clearest of all, and validating user input up front is often more readable than catching errors later.
Tip: Use the logging module’s logger.exception("Payment failed") inside a handler. It records your message and the full traceback, which a plain print does not.
Common mistakes
- Using a bare
except:. It also catchesKeyboardInterruptand typos such asNameError. Name the types you expect. - Silencing errors with
pass. An empty handler turns a loud failure into mysterious wrong data later. - Wrapping too much code in
try. Guard the one line that can fail, so you know what actually went wrong. - Using exceptions for normal flow. A loop ending or a value being missing is often better handled with a simple condition.
- Not testing error paths. Failure handling is code too. See how to check it with
pytest.raisesin our pytest tutorial.
Final thoughts
Good exception handling is selective. Catch what you can recover from, let everything else fail loudly, and keep the original cause when you translate errors. As a next step, look through one of your scripts for any bare except: and replace it with the specific exception you actually expect.
Frequently asked questions
What is the difference between an error and an exception in Python?
A syntax error stops code from running at all. An exception occurs while code runs, and your program can catch and handle it with try and except.
Does finally run if there is a return in try?
Yes. The finally block runs before the function actually returns, which is why it is reliable for cleanup. Avoid putting a return inside finally itself.
Should I catch Exception in Python?
Only at a top-level boundary, such as a web request handler or background worker loop, where you log the error and keep running. Elsewhere, catch specific types.
How do I print the full error message?
Use logger.exception inside a handler, or call traceback.print_exc from the traceback module. Both show the complete traceback, not only the message.
How do I create a custom exception in Python?
Define a class that inherits from Exception, such as class PaymentError(Exception). Then raise it like any other exception and catch it by name.
Keep learning
Need help with testing, WordPress or SEO?
We help teams ship better software and grow their sites. Tell us what you are working on and we will suggest a clear next step.