Introduction
In our last lesson, we focused on creating custom, domain-specific exceptions like InsufficientStockError. This is the correct approach for signaling problems unique to your application's business logic. We established that these exceptions create clean API boundaries, decoupling the calling code from implementation details.
Now, we'll address the other side of the coin: using Python's built-in exceptions. This lesson directly tackles the learning outcome: Raise specific, appropriate built-in exceptions like ValueError, TypeError, or KeyError.
Think of a function as having a contract: it expects arguments of a certain type and value to do its job correctly. When a caller violates this contract, your function should protest loudly and clearly. Instead of inventing a custom exception for a common programming error, you should use the standard, built-in signals that all Python developers understand. This practice makes your code predictable, interoperable, and easier to debug for anyone using it.
The Function Contract: Signaling Violations
Coming from Java, you might be accustomed to creating numerous custom checked exceptions for input validation. Python's philosophy is different. For violations of a function's basic contract (e.g., incorrect argument types or values), the idiomatic approach is to raise a built-in exception.
This is not a failure to handle an error; it is an enforcement of preconditions. Raising a TypeError when you expect an integer but receive a string is the correct, Pythonic way to signal that the calling code has made a mistake.
Let's look at the most common exceptions you'll raise to enforce these contracts.

ValueError vs. TypeError: The Critical Distinction
The two most important exceptions you will raise for input validation are ValueError and TypeError. Understanding the difference is crucial.
TypeError: Raised when an operation is applied to an object of an inappropriate type. The type itself is wrong.ValueError: Raised when an object has the right type, but an inappropriate value.
A single function can often demonstrate both. Consider a function to find prime numbers, which has a clear contract: it requires an integer argument that is 2 or greater.
Python's raise: Effectively Raising Exceptions in Your Code
The following article from Real Python provides a perfect example of a function that enforces its contract by raising both TypeError and ValueError. This clearly illustrates the distinction.
Read the section titled 'Raising Exceptions Conditionally'. Study the is_prime function. Note how it first checks the type with isinstance and raises a TypeError, and then checks the value and raises a ValueError. This is the standard pattern.
Let's break down the pattern from the article:
from math import sqrt
def is_prime(number):
# Contract Check 1: Is the TYPE correct?
if not isinstance(number, int):
raise TypeError(f"integer number expected, got {type(number).__name__}")
# Contract Check 2: Is the VALUE appropriate?
if number < 2:
raise ValueError(f"integer above 1 expected, got {number}")
# If the contract is met, proceed with the logic
for candidate in range(2, int(sqrt(number)) + 1):
if number % candidate == 0:
return False
return True
# --- Examples of contract violations ---
try:
is_prime("hello")
except TypeError as e:
print(f"Caught expected error: {e}")
try:
is_prime(-5)
except ValueError as e:
print(f"Caught expected error: {e}")
# --- Example of valid usage ---
print(f"Is 13 prime? {is_prime(13)}")
This "guard clause" pattern—checking preconditions at the very beginning of the function and raising exceptions immediately—is a cornerstone of clean, defensive Python code.
Anti-Pattern: Returning None or Error Codes
A common practice in other languages (and a frequent "Java-ism" in Python) is to return a special value like None, -1, or False to indicate an error. This forces the calling code to perform an explicit check on the return value, leading to cluttered logic.
Bad: Returning an error code
def calculate_average(grades):
if not grades:
return -1 # Error code for empty list
return sum(grades) / len(grades)
# Caller has to remember what -1 means
result = calculate_average([])
if result == -1:
print("Error: Cannot calculate average of empty list.")
This approach is brittle. The caller might forget to check for -1, leading to silent failures or incorrect calculations later on. The Pythonic way is to fail fast and loudly by raising an exception.
My FAVORITE Error Handling Technique
This segment from ArjanCodes makes a compelling case against returning default values to hide errors. It reinforces the 'fail fast' philosophy, which is central to robust error handling.
Watch the segment from 06:12 to 07:18. The presenter explains why returning 0 instead of raising an error can hide serious underlying issues and lead to corrupted state, like in an accounting system.
Good: Raising a specific exception
def calculate_average(grades):
if not grades:
# The type is right (list), but the value (empty) is wrong.
raise ValueError("Cannot calculate average of an empty list of grades.")
return sum(grades) / len(grades)
# Caller uses a try/except block for clear error handling
try:
calculate_average([])
except ValueError as e:
print(f"Error: {e}")
This is cleaner, more explicit, and leverages Python's error handling flow instead of fighting against it.
KeyError and IndexError: For Custom Containers
You typically won't raise KeyError or IndexError directly in everyday application code because Python's built-in dict and list types raise them for you.
However, you should raise them if you are creating your own custom container classes and want them to behave like standard Python sequences or mappings. This ensures your custom objects feel "native."
Python's Built-in Exceptions: A Walkthrough With Examples
This Real Python article provides an excellent example of raising IndexError within a custom Stack class. This is the canonical use case for raising this exception yourself.
Read the section on IndexError and study the Stack class example. Pay close attention to the __getitem__ method. It catches the IndexError from the internal list and re-raises it with a more informative message. This maintains the expected IndexError interface for a sequence-like object.
The key takeaway is that you raise IndexError or KeyError to fulfill the contract of a sequence or mapping protocol, making your custom objects compatible with the rest of the Python ecosystem.
Exercise: Identify the Right Exception
For each of the scenarios below, decide which built-in exception is most appropriate to raise.
- A function
set_user_role(user_id: int, role: str)receives arolethat is not one of the predefined valid roles (e.g., 'admin', 'editor', 'viewer'). - A function
connect_to_database(config: dict)is passed a string instead of a dictionary for theconfigparameter. - A function
get_monthly_sales(month: int)is called withmonth = 13. - Your custom
TimeSheetclass, which stores entries by date, is asked for an entry for a date that doesn't exist. Your class is designed to behave like a dictionary where dates are keys.
Answers
- `ValueError`. The type of
roleis correct (a string), but its value is not in the allowed set. - `TypeError`. The function's contract requires a
dict, but it received astr. The type is wrong. - `ValueError`. The type of
monthis correct (an integer), but its value is outside the valid range of 1-12. - `KeyError`. Since your class is meant to emulate a dictionary, you should raise a
KeyErrorto conform to the mapping protocol when a key (the date) is not found.
Conclusion
In this lesson, you've learned when and why to raise specific built-in exceptions. This is a fundamental aspect of writing clear, maintainable, and Pythonic code.
Key Takeaways:
- Raise built-in exceptions to enforce a function's contract. Don't return error codes or
None. - Use
TypeErrorfor wrong types andValueErrorfor right types but inappropriate values. This is the most common and important distinction. - Use
KeyErrorandIndexErrorwhen creating custom mapping or sequence types to make them behave like native Python containers. - Employ guard clauses at the beginning of your functions to validate inputs and fail fast, which keeps your main logic clean and un-nested.
In the previous lesson, you learned to create custom exceptions for domain errors. In this one, you learned to use built-in exceptions for contract violations. In our next lesson, we will complete the picture by focusing on the other side: how to catch exceptions effectively. We'll cover why you must catch specific exceptions and avoid the dangerous except: and except Exception: anti-patterns.