Python
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax and Data Types
-
Section 3: Collections
-
39: Set Operations: Union, Intersection, Difference
-
Section 4: Control Flow
-
Section 5: Functions
-
Section 6: Turtle Graphics and Early Practice Projects
-
Section 7: Working with Files and I/O
-
Section 8: Regular Expressions
-
Section 9: Object-Oriented Python
-
Section 10: Error Handling
-
Section 11: Modules and Packages
-
Section 12: Iterators, Generators, and Functional Tools
-
Section 13: Decorators and Metaprogramming
-
Section 14: Concurrency and Parallelism
-
Section 15: Working with Dates, Times, and Numbers
-
Section 16: Standard Library Deep Dive I: Data Structures
-
Section 17: Standard Library Deep Dive II: System and Introspection
-
Section 18: Standard Library Deep Dive III: Security and Encoding
-
Section 19: Standard Library Deep Dive IV: Text and Data Utilities
-
Section 20: Networking and Web Basics
-
Section 21: Working with Databases
-
Section 22: Testing and Quality
-
Section 23: Advanced Typing
-
Section 24: Context Managers and Resource Handling
-
Section 25: Text, Unicode, and Binary Data
-
Section 26: More Functional and Iteration Tools
-
Section 27: Data Validation and Configuration
-
Section 28: Working with Images and Media
-
Section 29: Property-Based and Documentation Testing
-
Section 30: Packaging and Deployment
-
Section 31: Performance and Internals
-
Section 32: Design Patterns in Python
-
Section 33: GUI Programming
-
Section 34: Security Basics
-
Section 35: Data Structures and Algorithms
-
Section 36: Practical Projects
-
Section 37: Capstone Projects
-
Section 38: Interview and Algorithm Practice
-
Section 39: Writing Idiomatic Python
144: Exception Chaining with raise ... from
I was working on a project recently where I had a custom configuration loader. The goal was simple: read a JSON file and return a dictionary. But since this was part of a larger library, I didn't want the end-user to have to deal with a json.JSONDecodeError. I wanted them to see a ConfigError, which is a custom exception I defined for this specific module. It makes the API cleaner.
The problem with losing the trail
My first instinct was to just catch the JSON error and throw my own. Here is how I started:
import json
class ConfigError(Exception):
pass
def load_config(path):
try:
with open(path, 'r') as f:
return json.load(f)
except json.JSONDecodeError:
raise ConfigError(f"The config file at {path} is malformed.")
# Let's test it with a broken file
with open("config.json", "w") as f:
f.write("{ 'bad_json': True ") # Missing closing brace
try:
load_config("config.json")
except ConfigError as e:
print(f"Caught: {e}")
At first glance, this looks perfect. The user gets the ConfigError I promised. But then I imagine myself as the developer trying to debug this in production. If I only see "The config file at config.json is malformed," I have no idea why it's malformed. Was there a missing comma? A trailing quote? All that precious metadata inside the JSONDecodeError (like the line and column number) is gone. I've effectively deleted the evidence.
That "During handling" awkwardness
Now, you might think, "Why not just let both exceptions bubble up?" If I change the code to raise the new error without catching the old one in a way that suppresses it, or if I just raise the new error inside the except block (which I did above), Python actually tries to be helpful. If you look at a full traceback for the code above, Python says: "During handling of the above exception, another exception occurred."
That phrasing is a bit ambiguous. It sounds like an accident—like the code crashed while it was trying to handle a crash. It doesn't explicitly say that the JSONDecodeError caused the ConfigError; it just says they happened in sequence. When I'm debugging a complex system, "happened in sequence" isn't the same as "is the root cause."
Making the cause explicit
This is where raise ... from comes in. It allows us to establish a formal causal link between two exceptions. Let's adjust the load_config function:
def load_config(path):
try:
with open(path, 'r') as f:
return json.load(f)
except json.JSONDecodeError as e:
# We use 'from e' to chain the exceptions explicitly
raise ConfigError(f"The config file at {path} is malformed.") from e
Now, when this crashes, the traceback changes its language. Instead of "During handling...", it says: "The above exception was the direct cause of the following exception."
This is a small linguistic change with a huge impact on debugging. It tells anyone reading the logs: "I intentionally wrapped this low-level error into a high-level error." The original JSONDecodeError is preserved in the __cause__ attribute of the ConfigError. You get the clean API for your user, but you keep the forensic evidence for yourself.
When you actually want to hide the trail
I'll be honest: there are rare times when the original exception is just noise. Maybe you're wrapping a third-party library that throws a massive, ugly traceback for a common user error, and you know for a fact that the original error provides zero value to the developer.
In those cases, you can use from None:
try:
do_something_messy()
except MessyLibraryError:
raise ConfigError("Something went wrong") from None
This tells Python, "I know there was another exception, but please ignore it entirely." It suppresses the original traceback, leaving only your ConfigError. Use this sparingly—usually, you're better off keeping the chain intact.
📋 Practical Task
Exercise: Implementing a Database Wrapper with Exception Chaining
You are building a simplified database wrapper. You have a function execute_query(query) that simulates a database call. Sometimes it raises a ConnectionError (a built-in Python exception) if the "server" is down.
Your task is to create a custom exception called DatabaseWrapperError. You must wrap the execute_query function in a new function called safe_execute. If execute_query raises a ConnectionError, safe_execute should catch it and raise a DatabaseWrapperError with the message "The database operation failed.", but it must use exception chaining to preserve the original ConnectionError as the cause.
Requirements:
- Define the
DatabaseWrapperErrorclass. - Implement
safe_execute(query)using theraise ... fromsyntax. - Test your implementation by calling
safe_executewith a query that triggers the error and verify that the originalConnectionErroris linked.
# Starter code
import random
class DatabaseWrapperError(Exception):
pass
def execute_query(query):
# Simulate a random connection failure
if random.choice([True, False]):
raise ConnectionError("Server timed out at 192.168.1.1")
return "Query Success!"
# Your safe_execute function goes here...
There are no comments for now.