Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 64

Raising Built-in Exceptions

Recommended resources

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.

Common Python Exceptions Overview
A quick reference for some of the most common built-in exceptions in Python. We will focus on raising `ValueError`, `TypeError`, `KeyError`, and `IndexError` in this lesson.

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.

  1. A function set_user_role(user_id: int, role: str) receives a role that is not one of the predefined valid roles (e.g., 'admin', 'editor', 'viewer').
  2. A function connect_to_database(config: dict) is passed a string instead of a dictionary for the config parameter.
  3. A function get_monthly_sales(month: int) is called with month = 13.
  4. Your custom TimeSheet class, 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
  1. `ValueError`. The type of role is correct (a string), but its value is not in the allowed set.
  2. `TypeError`. The function's contract requires a dict, but it received a str. The type is wrong.
  3. `ValueError`. The type of month is correct (an integer), but its value is outside the valid range of 1-12.
  4. `KeyError`. Since your class is meant to emulate a dictionary, you should raise a KeyError to 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 TypeError for wrong types and ValueError for right types but inappropriate values. This is the most common and important distinction.
  • Use KeyError and IndexError when 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.