Recommended resources
The freely available resources this lesson will direct you to.
- Exception Handling Tips in Python ⚠ Write Better Python Code Part 7www.youtube.com
- Python Pydantic Tutorial: Complete Data Validation Course (Used by FastAPI)www.youtube.com
- Python's raise: Effectively Raising Exceptions in Your Coderealpython.com
- 25. Validation, Exceptions, and Error Handlingfintechpython.pages.oit.duke.edu
Hello! Welcome to your seventh lesson in the "Production-Quality Python for Interviews" module.
In the last lesson, we explored the functools module, focusing on @lru_cache for performance optimization through memoization and partial for creating clean, specialized functions. These tools help us write code that is both efficient and highly readable.
Today, we shift our focus to a cornerstone of building reliable software: resilience. Any production system, like the Rudi service you built, must gracefully handle unexpected situations—from invalid user input and network failures to resource exhaustion. This lesson directly addresses the learning outcome: Implement robust error handling using custom exceptions and clear input validation.
We will cover:
- Input Validation: Using modern tools to create a strong first line of defense against bad data.
- Exception Handling: Mastering Python's
try...exceptmechanism to manage runtime errors. - Custom Exceptions: Designing specific error types to make your code more predictable and easier to debug.
- Best Practices: Applying patterns for chaining, re-raising, and resource management that are expected in production-grade code.
1. A Tale of Two Philosophies: LBYL vs. EAFP
In Python, there are two common approaches to handling potential errors:
-
LBYL (Look Before You Leap): You check for valid conditions before performing an action.
import os # LBYL: Check if the file exists first if os.path.exists('my_file.txt'): with open('my_file.txt', 'r') as f: print("File opened.") else: print("File not found.") -
EAFP (Easier to Ask for Forgiveness than Permission): You assume success and handle the failure if it occurs.
# EAFP: Try to open the file and handle the error try: with open('my_file.txt', 'r') as f: print("File opened.") except FileNotFoundError: print("File not found.")
Python's culture often leans towards EAFP. Why? One key reason, which will be very familiar from your distributed systems work, is that it helps avoid race conditions. In the LBYL example, another process could delete the file between the os.path.exists() check and the open() call. The EAFP approach is atomic in that regard—it just tries the operation.
However, LBYL is essential for input validation, where you want to reject invalid data before it enters your system's core logic. The key is to know when to apply each pattern.
Effectively Raising Exceptions in Your Code
The article 'Effectively Raising Exceptions in Your Code' from Real Python has a great section that frames this EAFP vs. LBYL discussion.
Read the section 'Deciding When to Raise Exceptions'. Focus on the comparison between the error-code-checking style (LBYL) and Python's exception-handling approach (EAFP).
2. Input Validation: The First Line of Defense with Pydantic
Before your code can fail at runtime, you must first ensure the data it receives is valid. Writing manual validation logic with if/else statements is verbose, error-prone, and often provides poor feedback by failing on the first error it finds.
In modern Python, the go-to library for production-grade data validation is Pydantic. It uses Python's type hints to create a declarative, powerful, and easy-to-use validation layer. It's a key component of frameworks like FastAPI and is invaluable for defining clear API contracts.
This video from Corey Schafer is an excellent, comprehensive introduction to Pydantic.
Python Pydantic Tutorial: Complete Data Validation Course (Used by FastAPI)
This Pydantic tutorial perfectly demonstrates the incremental complexity you prefer. It starts by showing the pain of manual validation and then builds up to advanced Pydantic features.
This video is rich with information. I recommend watching these key segments to get a strong grasp of Pydantic's core power: The 'Why' (00:00 - 03:38): Understand the problems Pydantic solves compared to manual validation. Basic Validation (03:38 - 10:55): See how to create a BaseModel and how Pydantic automatically validates types and required fields, collecting all errors at once. Handling Errors & Coercion (17:47 - 21:47): Learn how to catch ValidationError and understand Pydantic's default behavior of type coercion (e.g., string '123' to integer 123). Field Constraints (34:00 - 40:32): Explore how to add constraints like min_length, max_length, and gt (greater than) using Annotated and Field. Custom Validators (48:47 - 01:00:09): Learn how to write your own validation logic using the @field_validator and @model_validator decorators for more complex rules (like checking if two password fields match).
When you define a Pydantic model, you're essentially creating a schema for your data. If validation fails, Pydantic raises a ValidationError with a detailed JSON-like structure explaining every issue, which is exactly what a client application needs to display useful error messages.
This is what a real-world API error response, likely powered by a validation library like Pydantic, looks like:

For a system like Rudi, you could use Pydantic to define the expected request payload, ensuring that fields like model_id, prompt_tokens, and priority are of the correct type and within valid ranges before they ever hit your scheduling logic.
3. Handling Runtime Errors
Once input is validated, your application logic runs. This is where runtime exceptions—FileNotFoundError, KeyError, database connection errors, etc.—can occur.
Custom Exception Hierarchies
While you can catch built-in exceptions like ValueError, it's a best practice to define your own custom exceptions for errors specific to your application's domain. This makes your error handling code more readable and allows you to catch specific application-level failures.
For example, in your scheduling service, you could define a hierarchy:
class RudiError(Exception):
"""Base exception for the Rudi service."""
pass
class SchedulingTimeoutError(RudiError):
"""Raised when a request cannot be scheduled within its timeout."""
pass
class NoCapacityError(RudiError):
"""Raised when no inference capacity is available for a given model."""
pass
With this, a caller can choose to handle a specific NoCapacityError differently from a generic RudiError.
Let's watch a video that demonstrates this concept in the context of a web API.
Exception Handling Tips in Python ⚠ Write Better Python Code Part 7
This video from ArjanCodes shows how to translate low-level errors into high-level, meaningful custom exceptions, a key pattern for building layered applications.
Watch from 06:52 to 11:26. Pay close attention to how he: Defines custom exceptions like NotFoundError and NotAuthorizedError. Catches a low-level OperationalError from the database. Raises a high-level NotFoundError that the API layer can understand and translate into a 404 HTTP status code. This decoupling is a crucial architectural pattern.
Exception Chaining: Preserving Context
When you catch a low-level exception and raise a higher-level one, you risk losing the original traceback, which is critical for debugging. Python solves this with exception chaining using the from keyword.
def get_user_from_db(user_id):
try:
# This might raise a database-specific error
return db.query("SELECT * FROM users WHERE id = ?", user_id)
except db.ConnectionError as e:
# Chain the exceptions. The original db.ConnectionError will be preserved.
raise RudiError("Failed to connect to database") from e
When this code is run, the traceback will clearly state that the db.ConnectionError was the direct cause of the RudiError. This is invaluable in complex systems.
You can also use from None to explicitly suppress the original exception's context if you believe it's just noise and not helpful for the caller.
Effectively Raising Exceptions in Your Code
The Real Python article 'Effectively Raising Exceptions in Your Code' provides a detailed look at the mechanics of raising and chaining exceptions.
Please read the sections 'Reraising a Previous Exception' and 'Chaining Exceptions With the from Clause'. These sections clearly explain the difference between re-raising an exception and using the from clause to chain them, providing excellent examples.
Resource Management: finally and Context Managers
A common source of bugs in error handling is failing to clean up resources, like open files or network connections. Your Rust background gives you a deep appreciation for this via the RAII pattern. In Python, this is handled by the finally block or, more elegantly, with context managers (the with statement).
A finally block guarantees that a piece of code will run, regardless of whether an exception was raised in the try block.
f = open('data.txt', 'w')
try:
f.write("important data")
# ... some operation that might fail
finally:
# This will always run, ensuring the file is closed.
print("Closing file.")
f.close()
While finally works, the Pythonic way to manage resources is with the with statement, which wraps this logic up cleanly. We'll explore implementing our own context managers in a later lesson, but for now, know that it's the standard for resource safety. The ArjanCodes video (LINK, from 08:43 to 15:32) also provides a good visual walkthrough of refactoring from finally to a custom context manager.
4. Production-Quality Error Handling: Best Practices
Let's consolidate what we've learned into a set of best practices.
- Be Specific, Catch Specific: Always catch the most specific exception possible. Avoid broad
except Exception:clauses, and never use a bareexcept:, as it can hide critical errors likeSystemExitorKeyboardInterrupt. - Fail Fast and Validate Early: Use tools like Pydantic to validate data at the boundaries of your system. Don't let invalid data propagate.
- Provide Context: Use custom exception types and
raise ... from ...to give developers clear, actionable error reports. - Don't Silence Errors: Avoid
except: pass. If you truly intend to ignore an exception, add a comment explaining why it's safe to do so and consider logging it. assertis for Debugging, Not Validation: Theassertstatement is a tool for developers to check for internal inconsistencies during development. It should not be used for runtime input validation, as assertions can be globally disabled in production environments (python -O my_app.py), silently removing your checks.
Validation, Exceptions, and Error Handling
This page from a Duke University course provides a good summary of error handling techniques and best practices.
Skim through the sections 'Exception Best Practices', 'Assertion Syntax', and 'Discussion'. These reinforce the key principles of designing robust error handling logic.
Conclusion
You now have a framework for building resilient Python applications that can anticipate, handle, and report errors effectively. This is a non-negotiable skill for senior engineering roles and a frequent topic in system design and coding interviews.
Key Takeaways:
- Input Validation is your first line of defense. Use Pydantic to define declarative data schemas and get clear, comprehensive validation errors.
- Custom Exceptions add semantic meaning to your error conditions, allowing for more granular and readable error handling logic.
- Exception Chaining (
raise ... from ...) is crucial for debugging, as it preserves the original cause of an error. - Use the
withstatement (context managers) for automatic and robust resource cleanup, which is Python's answer to Rust's RAII. assertis a developer tool for sanity-checking assumptions, whileraiseis for handling runtime errors that can occur in production.
Next Lesson
This lesson concludes our initial module on "Production-Quality Python for Interviews." You've built a strong foundation in using Python's advanced standard library features and writing resilient code.
Our next lesson will begin a new module, "Python Concurrency and Parallelism," starting with a topic you know well from your Rust/Tokio experience. The learning outcome is: Explain the impact of the GIL and determine when to use threading, multiprocessing, or asyncio. We will map the concurrency patterns you're familiar with to their Python equivalents and discuss the unique challenges and opportunities presented by Python's Global Interpreter Lock (GIL).