Course
When Python code runs into problems at runtime, it often raises an exception. Left unhandled, exceptions will crash your program. However, with try-except
blocks, you can catch them, recover gracefully, and keep your application running.
This tutorial isn’t about the basics of exceptions, we already cover that in our Exception & Error Handling in Python guide. Instead, here we’ll take a deep dive into try-except
in practice: how to structure your blocks, avoid common mistakes, and apply best practices that make your code more reliable in real-world scenarios.
By the end, you’ll understand not only how try-except
works, but also how to use it the Pythonic way; writing error-handling code that’s clear, maintainable, and production-ready.
If you’re still on your Python learning journey, I recommend checking out the Python Programming Fundamentals skill track, which will help you build all the essential skills.
Why Focus on try-except?
Error handling in Python is fundamentally about designing programs that can deal with the unexpected. And while there are many tools for handling errors, try-except
is the backbone of Python’s approach.
Why does it matter so much?
- It reflects Python’s philosophy. In Python, the common style is EAFP; Easier to Ask Forgiveness than Permission. Instead of checking everything up front (does this file exist? is this input valid? is the server available?), you just try the operation. If it fails, you catch the exception and handle it. This leads to shorter, cleaner code.
- It keeps code running in real-world environments. User input will be messy, files will go missing, APIs will fail, and networks will drop. With
try-except
, you can write code that doesn’t collapse at the first problem but instead reacts intelligently. - It’s flexible enough for beginners and professionals. A beginner might use
try-except
to catch aValueError
when converting input to an integer. A production system might use it to log errors, retry failed operations, or raise custom exceptions that make debugging easier.
That’s why this article zooms in on try-except
. Mastering it is the difference between writing scripts that only work in perfect conditions and writing software that’s robust, maintainable, and production-ready.
Anatomy of a Try‑Except Block
At its simplest:
try:
risky_thing()
except SomeError:
handle_it()
When Python encounters an error inside the try
block, it jumps to the matching except
block instead of crashing the program.
You can also extend this pattern with else
and finally
clauses:
try:
do_something()
except ValueError:
print("Bad value!")
else:
print("All good.")
finally:
clean_up()
Here’s a basic working example:
try:
x = int(input("Enter a number: "))
except ValueError:
print("That wasn’t a number.")
else:
print("You entered", x)
finally:
print("Done.")
The except
handles the error, else runs only if things worked, and finally always runs, even if you hit Ctrl‑C or exit.
You can master the fundamentals of user input handling in Python, from basic prompts to advanced validation and error handling techniques using our Python User Input: Handling, Validation, and Best Practices tutorial.
Handling Multiple Exceptions
Sometimes, one except
just doesn’t cut it. Different code paths can fail in different ways, and catching everything under the sun with a bare except: isn’t exactly helpful; it can hide bugs and make debugging a headache.
Say you're trying to convert something to an integer and then divide it. Here's what not to do:
try:
result = int(user_input) / 2
except:
print("Something went wrong.")
This catches everything, including things you probably didn’t intend to hide, like typos in variable names or unexpected exceptions that point to real problems.
Instead, be specific:
try:
result = int(user_input) / 2
except ValueError:
print("That wasn't a number.")
except ZeroDivisionError:
print("Division by zero?")
If you’ve got several exception types that need to be treated the same way, you can group them like this:
try:
result = some_function()
except (TypeError, ValueError):
print("Something was wrong with the data.")
This way, you’re still clear about what might go wrong, without duplicating the same block over and over.
And then there’s except Exception
, which is better than a bare except
, but still a bit too wide. You’re better off targeting the specific errors you expect.
Using else and finally Blocks
When the code inside the try
block succeeds and no exceptions are raised, Python executes the else
block.
try:
user_id = get_user_id()
except LookupError:
print("User not found.")
else:
print("Welcome, user", user_id)
This helps you separate your exception-handling logic from the happy path, making it easier to read.
Then there’s finally
. It doesn’t care what happened; it runs no matter what. Exception? Still runs. No exception? Still runs. Program crashes with Ctrl‑C? Still runs. Great for cleanup stuff:
try:
f = open("data.txt")
process(f)
except IOError:
print("Couldn’t open file.")
finally:
f.close()
Just remember, if the file never opened, f
might not even exist, so be careful. You could use Python’s context manager (with open(...) as f:
) instead when working with files. It’s safer.
If you want to learn key techniques, such as exception handling and error prevention, to handle the KeyError
exception in Python effectively, I recommend our Python KeyError Exceptions and How to Fix Them tutorial.
Python try-except Best Practices and Common Pitfalls
Let’s be honest: writing try‑except
blocks is a bit of an art. Done well, they keep your program from crashing. Done poorly, and they bury bugs so deep you won’t notice them until your app crashes and users are complaining.
Here are some habits worth building:
Keep try
blocks tight. Don’t wrap a hundred lines of code inside a single try. That just makes it harder to know what caused the problem. Instead, wrap only the code that might fail:
# Good
try:
value = int(data)
except ValueError:
print("Couldn’t convert.")
# Bad
try:
# Tons of unrelated logic
value = int(data)
do_more()
something_else()
except ValueError:
print("Huh?")
Avoid checking conditions before doing the action if it’s easier just to try and catch the error. Python has a name for this: EAFP, Easier to Ask Forgiveness than Permission. If you're checking whether a file exists and then opening it, you're creating a race condition. Instead:
try:
with open("file.txt") as f:
content = f.read()
except FileNotFoundError:
print("No file.")
This pattern avoids a common problem where the file might disappear between the check and the open call. Just try the thing. If it fails, catch the failure.
Also, avoid silencing errors by catching everything and doing nothing. Don’t do this:
try:
something()
except:
pass
Unless you really know what you’re doing, this is where bugs go to hide and multiply. If you must silence, be specific:
try:
something()
except TimeoutError:
# okay to ignore in this case
pass
And log the errors somewhere. Swallowing them completely means future you will have no clue what went wrong.
Built-in vs Custom Exceptions
Python comes with plenty of exceptions baked in, and they cover a surprising number of cases. You’ve probably bumped into some already:
ValueError
: when something has the right type but an invalid value, likeint("hello")
.TypeError
: when you try to use an operation on the wrong type, like adding a string and a number.ZeroDivisionError
: you guessed it, dividing by zero.FileNotFoundError
: trying to open a file that’s not there.KeyError
: when a key is missing in a dictionary.
These are useful, and in most scripts or apps, they’re more than enough. But sometimes you’ll want to raise something more descriptive, something that makes sense in your project’s world, not just Python’s.
Let’s say you’re writing an app that processes online orders, and you want to raise an error when a user tries to buy something that’s out of stock. You could just use ValueError
, but that’s a bit generic. It doesn’t tell the next person reading your code what happened.
Here’s where custom exceptions shine.
class OutOfStockError(Exception):
pass
def check_inventory(product_id):
if not in_stock(product_id):
raise OutOfStockError(f"Product {product_id} is out of stock.")
By creating the custom exception, you're adding a layer of meaning. You're also giving yourself more control; you can catch just this one specific situation:
try:
check_inventory("shirt-001")
except OutOfStockError:
print("Sorry, that item is sold out.")
It’s a small thing, but it makes your code easier to understand and maintain, especially in bigger projects. And it plays well with logging and monitoring, custom exception names are much easier to search for than a vague ValueError
.
Logging, Re-Raising, and Exception Chaining
There’s this moment that happens when debugging: you see a traceback, stare at it, and realize you have no clue why something failed. That’s where logging comes in. It’s not just about recording errors, it’s about giving future you (or your team) the breadcrumbs to figure out what went wrong.
Let’s say you’re catching an exception and want to log it before moving on:
import logging
logging.basicConfig(level=logging.ERROR)
try:
do_something()
except ValueError as e:
logging.error("Failed to do something: %s", e)
raise
That raise
at the end is important, it rethrows the same exception after logging it. Without it, you’ve just swallowed the error. Sometimes that’s fine, but usually, it’s not.
Then there's the raise from
trick. This one’s for when you’re handling one error but need to raise another, and you don’t want to lose the original one. Python lets you chain them:
try:
connect_to_database()
except TimeoutError as e:
raise ConnectionError("Database unavailable.") from e
This way, the traceback tells the whole story. You get the new ConnectionError
, but you also see the TimeoutError
that caused it.
You can also suppress the original error (which you usually shouldn’t) like this:
raise ConnectionError("Just this error, nothing else.") from None
But unless you’ve got a good reason, keeping the full chain helps everyone understand what went wrong and how it snowballed.
You can learn about the fundamentals of logging in Python from our Logging in Python Tutorial.
Real-World Examples and Use Cases
It’s one thing to talk about exception handling in the abstract, but it clicks when you see it in actual code.
Take user input, for example. Ask anyone who’s ever built a command-line tool or form validator: users will enter the weirdest things. You ask for a number? Someone will type “twelve.” Or paste in a phone number. Or just press Enter. It happens.
Instead of writing a long list of “what if” checks, you can do this:
while True:
user_input = input("Enter a number: ")
try:
number = int(user_input)
break
except ValueError:
print("Try again with a whole number.")
This code doesn’t panic when it gets garbage input. It tells the user to try again and loops until things look good. Much cleaner than stacking if statements for every edge case.
Here’s another one: reading from a file that might not exist.
try:
with open("config.json") as f:
settings = f.read()
except FileNotFoundError:
print("Missing config file. Using defaults.")
settings = "{}"
No need to check if the file’s there. Just try to open it, and if it fails, move on. If you’d tried checking beforehand (os.path.exists()
), someone else might’ve deleted the file between the check and the open. That’s a race condition, not something you want to debug.
Network requests are another goldmine for exceptions. You can’t always trust the internet to behave. Servers go down. Connections drop. DNS fails. So if you’re doing something like this:
import requests
try:
response = requests.get("https://example.com/data")
response.raise_for_status()
except requests.exceptions.RequestException as e:
print("Network problem:", e)
That RequestException
base class conveniently catches pretty much anything requests
might raise, from timeouts to bad responses. You don’t have to write ten different except
blocks unless you want to handle them differently.
And if you’re writing automation scripts or backend services, wrapping key logic in try‑except blocks can mean the difference between one task failing and the whole system going dark. You want errors logged, recoverable tasks retried, and unrecoverable ones shut down cleanly, not with cryptic stack traces scrolling endlessly across your logs.
Learn about Python automation, including fundamental concepts, key libraries, working with data, using AI enhancements, and best practices from our Python Automation: A Complete Guide tutorial.
Try-except Python Advanced Uses
By now, you've probably seen how flexible try‑except
can be. But flexibility cuts both ways. It’s easy to go from helpful to sloppy without meaning to. Here’s how to keep things in check.
Catch specific exceptions. If you know what could go wrong, name it:
try:
result = int(data)
except ValueError:
# Only catches invalid numbers, not everything else under the sun
Avoid bare except
:. Don’t do this unless you’re handling something very special. It’ll catch things like KeyboardInterrupt
, SystemExit
, and other things you probably don’t want to silence.
Use else
and finally
when they make the code clearer. Don’t force them in just because they exist. If the normal path of your code is getting buried inside a try
, maybe move it to an else
.
Keep your try
blocks small. The more you include, the harder it is to figure out which line caused the error. Wrap just the part that might fail.
Log errors when needed, especially in production. Even if you're not crashing, knowing what failed (and when) makes debugging so much easier later.
Custom exceptions aren’t mandatory, but they help. If you’ve got app-specific problems, define your own errors. They can make logs more readable and your code more self-explanatory.
One last thing: don’t use exceptions for flow control unless there’s no better way. It’s tempting to write logic like “try this; if it fails, do that,” but if it’s something you expect to happen all the time, there’s probably a cleaner way.
Conclusion
As I’ve explained throughout this article, exception handling isn’t just about avoiding crashes. It’s about writing code that expects trouble, handles it without drama, and keeps going, or shuts down in a way that makes sense. It’s the difference between a user getting a helpful message and getting dumped back to the terminal with a long, unreadable traceback.
I highly recommend learning more about exceptions, with some hands-on exercises, in our Catching Exceptions in Python chapter of our OOP in Python course.
Python try-except FAQs
What’s the difference between an error and an exception in Python?
An error usually refers to a problem that prevents Python from even starting your code, like a missing colon or a typo in a keyword. These are syntax errors. An exception occurs while the code is running, such as trying to divide by zero or opening a file that doesn't exist. Exceptions can be caught and handled so your program doesn’t crash.
Is it bad to use a bare except in Python?
Yes, in most cases. A bare except
catches everything, including stuff you didn’t mean to catch, like keyboard interrupts or system exit signals. That makes debugging tough. It’s better to catch specific exceptions, such as ValueError
or FileNotFoundError
, so you know exactly what you're handling.
When should I use else in a try-except block?
Use else when you want to run some code only if no exception occurred in the try block. It helps keep your success-path logic separate from the error-handling logic, which can make your code easier to read and maintain.
What’s the point of finally if I already have an except block?
finally
runs no matter what, whether there’s an error or not. It’s perfect for cleanup: closing files, releasing resources, rolling back transactions, and so on. Even if an error happens or you exit early, finally will still run.
Should I always use `try-except` instead of checking conditions first?
Not always, but often it’s better to just try
the thing and catch
the error if it fails. Python developers call this EAFP,“Easier to Ask Forgiveness than Permission.” It’s faster and avoids certain bugs, especially when something might change between the check and the action (like a file being deleted).
