Back to blog

Python Try and Except: How to Handle Errors Without Crashing Your Script

An unhandled runtime error crashes a Python program immediately. The try/except is the standard mechanism for handling those failures and keeping the script under control. This guide covers all the exception-handling clauses: try, except, else, finally, and raise, alongside practical guidelines for keeping exception handlers narrow, explicit, and maintainable.

Python Try and Except

TL;DR

  • Wrap risky code in try and handle failures in except, so a single runtime error doesn't crash your entire script.
  • Catch the narrowest exception type first. A broad handler hides unrelated bugs behind one response.
  • Use else for logic that runs only on success, and finally for cleanup that runs regardless of the outcome.
  • Use raise to signal an invalid state or re-raise a caught exception after logging, so failures stay visible instead of being silently swallowed.

How try and except blocks work

Any Python line that depends on something unpredictable can raise an exception. Files, networks, user input, and external APIs all fall into that category. When an exception goes unhandled, the current program terminates.

try and except address that directly. You place the risky code inside a try block; if it raises an exception, Python immediately exits the block and runs the matching except clause instead. If nothing goes wrong, except is skipped entirely.

Let’s show how that works in practice using a simple HTTP request. The URL below points to a test endpoint that delays its response by 10 seconds. With a 3-second timeout set, we should hit a Timeout exception:

import requests
response = requests.get("https://httpbin.org/delay/10", timeout=3) # 3 seconds
data = response.json()
print(data["url"])
# rest of script...

If the request goes beyond 3 seconds, the Timeout exception stops everything at once - the print() function never runs as a result. Any logic after this block is skipped, too.

Now add try and except to the same request:

import requests
try:
response = requests.get("https://httpbin.org/delay/10", timeout=3)
data = response.json()
print(data["url"])
except requests.exceptions.Timeout:
print("Request timed out – skipping this URL")
# rest of script...

The request can still fail, but the script can now recover gracefully. Python leaves the try block as soon as requests.get() raises Timeout and moves to except. That means the remaining lines inside the try block are skipped. On a successful request, Python ignores except and continues with the rest of the script.

try/except goes beyond crash prevention; it follows a Pythonic philosophy for handling uncertainties: the EAFP (Easier to Ask Forgiveness than Permission). Instead of checking every condition before a request, you make the request and handle the failure if it happens.

The alternative is LBYL (Look Before You Leap), which relies on if/else guard clauses. But it’s a more verbose and often less reliable approach, since conditions don't always hold. The millisecond between checking and acting is enough for a file to disappear, a connection to drop, a field to go missing.

Catching specific exceptions

When a request fails, it could be the network, the response body, or a missing field. Without a named exception type, one handler catches all 3 and your script keeps running with no indication of which step broke.

Specifying the exception type fixes that. The handler only fires for that specific failure; everything else surfaces as an unhandled error.

Python has built-in types for common failures, including ValueError, TypeError, KeyError, IndexError, FileNotFoundError, and more. Third-party libraries define theirs as well, requests.exceptions.Timeout from earlier is a good example.

To read the actual error message, alias the exception to a variable using as e. You can then print or log it:

except requests.exceptions.Timeout as e:
print(f"Request timed out: {e}")

That works when you're handling one failure mode. But a request can break in more than one way, and a single handler can't distinguish between them:

import requests
import json
url = "https://httpbin.org/json"
try:
response = requests.get(url, timeout=5)
data = response.json()
print(data["slideshow"]["title"])
except requests.exceptions.Timeout:
print(f"Request timed out: {url}")
except json.JSONDecodeError as e:
print(f"Response wasn't valid JSON: {e}")
except KeyError as e:
print(f"Expected key missing: {e}")
# rest of script...

The example above shows 2 things: how Python chooses an except block and why broad handlers are risky.

When you stack multiple except blocks, Python doesn't evaluate all of them. It reads from top to bottom and stops at the first match. That's why requests.exceptions.Timeout, json.JSONDecodeError, and KeyError can each lead to a different response.

Put a broad clause like except Exception first, and Python sends every matching runtime error there instead of reaching the more specific handlers below. That doesn't make except Exception wrong on its own. It means broad handlers belong at the end, after the narrow ones.

Bare except creates a different problem. It doesn't just catch runtime errors, it also catches KeyboardInterrupt and SystemExit, which can make a script harder to stop. If you need a broad fallback, except Exception is the safer choice because it still leaves those signals alone.

Catching multiple exceptions

Not every exception needs its own except block. Sometimes, 2 exceptions lead to the same response; as such, one handler is enough. For instance, a scraped price field might be missing entirely (KeyError) or present but non-numeric (ValueError). Either way, the product is unusable. Without a way to group them, you end up here:

try:
price = float(product["price"])
except KeyError:
print("Skipping product — bad price field")
continue
except ValueError:
print("Skipping product — bad price field")
continue

Both exceptions lead to the same message, so separate handlers only repeat the same code. To catch multiple exceptions, you pass them as a tuple:

try:
price = float(product["price"])
except (KeyError, ValueError) as e:
print(f"Skipping product — bad price field: {e}")
continue

What makes this different from except Exception is intent. You've made a deliberate call that these failures share a response, rather than collapsing everything into one broad net.

Here's the rule of thumb: use grouped handling when multiple exceptions lead to the same action, and separate handlers when each exception needs a different action.

The else clause: separating success from failure handling

If you've used try/except before, in Python or any other language, your instinct is probably to put everything that should run on success at the bottom of the try block. And that works. So why does else exist?

Because "it works" hides a real problem.

When success logic sits inside a try block, any exception it raises is handed to your except handlers. A bug in your parsing code can look like a network failure, since both end up in the same handler. else lets you separate the two concerns.

The else block only runs when the try block completes without raising any exception. Think of else as a success-only block. If any except clause fires, the else block is skipped entirely. Here’s how that looks against a real page:

import requests
from bs4 import BeautifulSoup
url = "https://books.toscrape.com"
try:
response = requests.get(url, timeout=5)
response.raise_for_status()
except requests.exceptions.RequestException as e:
print(f"Request failed: {e}")
else:
soup = BeautifulSoup(response.text, "html.parser")
title = soup.find("h1").text
print(f"Page title: {title}")

The try block makes the request to Books to Scrape, except handles the request failures, and else runs only on success. If Beautiful Soup raises an error there, it surfaces as its own exception instead of getting absorbed by the connection error handler. 

The finally clause: running cleanup code

The finally block always runs at the end. It doesn't matter whether try succeeded, except ran, or Python is still raising an error. Unlike else, which only runs on success, and except, which runs on failure, finally does not depend on the outcome at all.

That makes it the right place for one thing: cleanup. Closing file handles, database connections, browser sessions – anything that needs to happen regardless of the result.

To put finally to work, here's a Playwright example that ensures the browser session always closes:

from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch()
try:
page = browser.new_page()
page.goto("https://books.toscrape.com")
title = page.title()
print(title)
except Exception as e:
print(f"Scrape failed: {e}")
finally:
browser.close()

Whether the scrape succeeds, throws, or crashes entirely, Playwright shuts the browser down. Without finally here, a mid-scrape failure leaves the browser process running, and piled-up orphaned instances drain memory really fast.

One thing to avoid: don't put return statements or business logic inside finally. A return inside finally silently suppresses any exception that was propagating – the kind of bug that wastes an entire day before you finally think to check finally.

Now that you have all 4 clauses in place, here's a quick reference table on how they divide the work:

Clause

When it runs

try

Always. Contains the code that might raise an exception

except

Runs only if the try block raises a matching exception

else

Runs only if the try block completes without raising an exception

finally

Always runs, regardless of whether an exception occurred

Nested try/except blocks

Nested try/except blocks make sense when an operation has 2 distinct failure points that need separate handling. For instance, during a scrape, the outer block catches the network failure, and the inner block catches what breaks during parsing:

import requests
from bs4 import BeautifulSoup
url = "https://books.toscrape.com/catalogue/category/books/mystery_3/index.html"
try:
response = requests.get(url, timeout=5)
response.raise_for_status()
except requests.exceptions.RequestException as e:
print(f"Could not reach {url}: {e}")
else:
try:
soup = BeautifulSoup(response.text, "html.parser")
title = soup.find("h1").text
print(f"Title: {title}")
except AttributeError as e:
print(f"Element not found: {e}")

soup.find() returns None when the element doesn't exist, and calling .text on None raises an AttributeError – a parsing failure that only makes sense to handle after a successful request. Any exception the inner except doesn't handle bubbles up to the outer block, which covers cases like a dropped connection or a timeout mid-scrape.

As a practical ceiling, stop at 2 levels of nesting. If you need a third, move the inner block into its own function with its own try/except. Deep nesting can quickly collapse into a pyramid of doom.

Raising exceptions intentionally with raise

The raise statement is the opposite side of exception handling. So far, the focus has been on catching exceptions with except. raise is how your code creates exceptions.

Two common forms:

  • Raising a new exception when your code detects an invalid state or unusable data.
  • Re-raising the current exception after logging or partially handling it.

See examples below:

# Raise a new exception when the price format is invalid
try:
price = float(product["price"])
except ValueError:
raise ValueError(f"Invalid price format: {product['price']!r}")
# Re-raise the current exception after logging it
try:
response = requests.get(url, timeout=5)
except requests.exceptions.Timeout:
print(f"Request timed out: {url}")
raise

In larger codebases, developers sometimes go for custom exceptions by inheriting from Python's built-in Exception class. That creates error types like ScraperError or ParseError that behave like normal exceptions but describe a more specific failure.

# Create a custom exception type
class ScraperError(Exception):
pass
# Raise the custom exception
raise ScraperError("Product page missing")

Best practices for exception handling

1. Catch the narrowest exception type you can

Always start by catching only what you expect. If you only expect a Timeout, don't catch Exception. Broad handlers hide bugs; a TypeError in your parsing logic looks like a network failure if both land in the same handler.

2. Keep try blocks small

Wrap only the lines that can actually raise the expected exception, not an entire function body. A large try block pulls unrelated code into the same handler, making it harder to tell where a failure actually originated.

3. Never write except: pass

Silencing exceptions without logging is how scraping scripts break in production without any trace of what went wrong. This gets worse in long-running scraping jobs where silent failures compound over hundreds of requests. At least, log the error before moving on.

4. Log with context, not just the exception

Using print(f"Failed: {e}") quickly becomes unhelpful once multiple requests start failing for different reasons. Include the URL, the operation, and the exception type. Robust request handling starts with knowing exactly where things broke.

5. Retry transient failures instead of swallowing them

A Timeout, a 429, or a temporary 503 still leaves the request failed if you just pass. Implement retry with backoff so transient errors get a second chance and persistent ones surface quickly.

But if 429 and 503 errors become persistent, retries alone won't solve the underlying rate-limit problem. Rotating residential proxies like Decodo residential proxies reduces that frequency by distributing requests across different IPs.

Enhance your web scraper with proxies

Claim your 3-day free trial of residential proxies and access 115M+ ethically-sourced IPs, advanced geo-targeting options, a 99.86% success rate, and more.

Final thoughts

Exception handling isn't defensive programming. It's how you make failure modes visible, debuggable, and transparent. A well-placed try/except goes beyond crash prevention; it shows the next developer what can fail and how the script responds.

Always write handlers that are narrow enough to catch the right error, specific enough to log something useful, and explicit enough to re-raise and not swallow what they can't fix.

About the author

Vilius Sakutis

Head of Partnerships

Vilius leads performance marketing initiatives with expertize rooted in affiliates and SaaS marketing strategies. Armed with a Master's in International Marketing and Management, he combines academic insight with hands-on experience to drive measurable results in digital marketing campaigns.

Connect with Vilius via LinkedIn

All information on Decodo Blog is provided on an as is basis and for informational purposes only. We make no representation and disclaim all liability with respect to your use of any information contained on Decodo Blog or any third-party websites that may belinked therein.

Frequently asked questions

What is try and except in Python?

try and except are Python's standard mechanisms for handling failures without crashing the script. The try block holds the code that might fail, and except runs the response when a matching exception is raised.

What is an exception in Python?

An exception is a runtime error Python raises when an operation fails, such as a missing file, a network timeout, or invalid input. Without a handler, the exception terminates the script immediately.

What can I use instead of try and except in Python?

The alternative is taking the LBYL (Look Before You Leap) route, where you use the if/else guard clause before each risky operation. However, it's more verbose and less reliable than try and except, because conditions can quickly change between the check and the call.

Is try/catch a bad practice?

No, try/catch (or try/except in Python) is only a bad practice when it's misused. For instance, when used to silence errors, replace conditional if checks, or catch exceptions indiscriminately. Narrow handlers that log and respond to specific failures make code more maintainable, not less.

Python Errors and Exceptions

Python Errors and Exceptions: An Ultimate Guide to Different Types and Solutions

In this article, we’ll explore the different kinds of errors and exceptions, what causes them, and provide solutions to solving them. No more headaches and cursing your code until it gets scared and starts working – master the language of Python to understand precisely what it wants from you.

Retry Failed Python Requests in 2026

There’s no reliable Python application that doesn’t have a built-in failed HTTP request handling. You could be fetching API data, scraping websites, or interacting with web services, but unexpected failures like timeouts, connection issues, or server errors can disrupt your workflow at any time. This blog post explores strategies to manage these failures using Python’s requests library, including retry logic, best practices, and techniques like integrating proxies or custom retry mechanisms.

Beautiful Soup Web Scraping: How to Parse Scraped HTML with Python

Web scraping with Python is a powerful technique for extracting valuable data from the web, enabling automation, analysis, and integration across various domains. Using libraries like Beautiful Soup and Requests, developers can efficiently parse HTML and XML documents, transforming unstructured web data into structured formats for further use. This guide explores essential tools and techniques to navigate the vast web and extract meaningful insights effortlessly.

© 2018-2026 decodo.com (formerly smartproxy.com). All Rights Reserved