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
146: Best Practices for Exception Granularity
Look, we've all been there. You're in a rush to get a feature working, you wrap your logic in a massive try...except Exception block, and you tell yourself you'll "clean it up later." It feels safe because the program doesn't crash, but in reality, you've just built a silent failure machine. When you catch every possible exception under one umbrella, you aren't handling errors; you're hiding them.
Let's look at a scenario we deal with constantly: reading a configuration file. Imagine you're writing a function that loads a JSON config and retrieves a specific API key. Here is the naive way to do it:
import json
def get_api_key(filepath):
try:
with open(filepath, 'r') as f:
config = json.load(f)
return config['api_key']
except Exception as e:
print(f"An error occurred: {e}")
return None
The Danger of the Blanket Catch
On the surface, this looks robust. If the file is missing, it doesn't crash. If the JSON is malformed, it doesn't crash. But here is where it breaks: what happens if you accidentally type config['api_key'] (a typo) instead of config? Python would normally raise a NameError, which is a programmer error, not a runtime environmental error. But because we used except Exception, that NameError is caught, printed as a generic "error occurred," and the function returns None.
I've spent hours debugging "NoneType" errors in production only to realize the root cause was a typo hidden inside a broad exception handler. When you catch everything, you treat a missing file (which is expected and recoverable) the same way you treat a syntax error or a memory failure (which should probably crash the app so you can fix the bug). You've effectively blinded your own debugger.
Precision Handling and Intent
The better way is to be granular. You want to catch only the exceptions you actually know how to recover from. If you can't fix the problem programmatically, let the exception bubble up. This makes your intent clear to anyone reading your code: "I expect the file might be missing, and I expect the JSON might be corrupted, but I do NOT expect the dictionary key to be missing."
import json
def get_api_key(filepath):
try:
with open(filepath, 'r') as f:
config = json.load(f)
return config['api_key']
except FileNotFoundError:
# We can handle this: maybe use a default value or alert the user
print("Config file missing. Using default settings.")
return "DEFAULT_KEY"
except json.JSONDecodeError:
# The file exists but is garbage. This is a critical configuration error.
print("Config file is corrupted. Please check the JSON syntax.")
raise
except KeyError:
# The file is valid JSON, but the specific key is missing.
print("Config file is missing the 'api_key' field.")
return None
Balancing Verbosity and Safety
Now, you might be thinking, "Do I really need five different except blocks for every single function?" Not necessarily. The trade-off here is between code brevity and diagnostic clarity. If three different errors all lead to the same recovery path—say, returning a default value—you can catch them in a single tuple: except (FileNotFoundError, KeyError):.
But the golden rule I follow is this: never catch Exception or BaseException unless you are writing a top-level logger or a main loop that must absolutely never die. For everything else, if you can't name the specific exception you're looking for, you probably aren't ready to handle that error yet. It's better to let the program crash during development than to spend three days hunting a silent bug in production.
📋 Practical Task
Refactoring a Brittle API Response Parser
You've inherited a piece of code that processes API responses. The current implementation uses a "catch-all" block that is making it impossible to tell if the API is down, if the response is malformed, or if the processing logic has a bug.
Your Task: Refactor the parse_user_data function. Replace the generic except Exception block with specific handlers for the following scenarios:
ValueError: Should print "Invalid data format" and returnNone.KeyError: Should print "Missing required user field" and returnNone.- Any other unexpected errors should be allowed to propagate (bubble up) so the developer can see the traceback.
def parse_user_data(response_json):
try:
# Assume response_json is a string that needs parsing
import json
data = json.loads(response_json)
# We expect a 'user' object with an 'id' and 'email'
user_id = data['user']['id']
email = data['user']['email']
# Convert ID to integer to ensure it's valid
return {"id": int(user_id), "email": email}
except Exception as e:
print(f"Error: {e}")
return None
# Test cases
print(parse_user_data('{"user": {"id": "123", "email": "test@example.com"}}')) # Should work
print(parse_user_data('{"user": {"email": "test@example.com"}}')) # Should trigger KeyError
print(parse_user_data('{"user": {"id": "abc", "email": "test@example.com"}}')) # Should trigger ValueError
print(parse_user_data('invalid json')) # Should raise json.JSONDecodeError (unhandled)
There are no comments for now.