DevHireLab
Tutorials
BootcampProblemsCode SimulatorAI InterviewSoonContact
DevHireLab
Tutorials
BootcampProblemsCode SimulatorAI InterviewSoonContact
Getting Started
01
What is Python and Why Learn It in 2026
02
Installing Python on Windows, Mac, and Linux
03
Setting up VS Code / PyCharm for Python
04
Understanding the Python Interpreter and REPL
05
Writing and Running Your First Python Program
06
Python Syntax and Indentation Rules
Variables and Data Types
01
Variables and Naming Conventions in Python
02
Numbers in Python (int, float, complex)
03
Strings and String Methods in Python
04
Booleans in Python
05
Type Conversion / Casting in Python
06
Understanding the None Type in Python
Operators
01
Arithmetic Operators in Python
02
Comparison Operators in Python
03
Logical Operators in Python
04
Assignment Operators in Python
05
Bitwise Operators in Python
06
Identity (is) and Membership (in) Operators
07
Walrus Operator (:=) in Python
Input, Output & Strings
01
input() and print() Functions in Python
02
f-strings and String Formatting in Python
03
String Slicing and Indexing in Python
04
Common String Methods (split, join, strip, replace)
05
Escape Characters in Python Strings
Control Flow
01
if, elif, else Statements in Python
02
Nested Conditionals in Python
03
for Loops in Python
04
while Loops in Python
05
break, continue, and pass Statements
06
Loop with else Clause in Python
07
match-case (Structural Pattern Matching)
Data Structures
01
Python Lists: Creation, Indexing, Slicing, Methods
02
Python Tuples and Immutability
03
Python Sets and Set Operations
04
Python Dictionaries: Keys, Values, and Methods
05
List Comprehensions in Python
06
Dictionary & Set Comprehensions in Python
07
Working with Nested Lists and Dictionaries
Functions
01
Defining and Calling Functions in Python
02
Function Arguments: Positional, Keyword, Default
03
*args and **kwargs in Python
04
Return Statements in Python Functions
05
Lambda Functions in Python
06
Recursion in Python
07
Variable Scope: Local, Global, Nonlocal
08
Docstrings and Function Annotations
Functional Programming Basics
01
map(), filter(), and reduce() in Python
02
Closures in Python Explained
03
Python Decorators: Basic to Advanced
04
Generators and yield in Python
Object-Oriented Programming (OOP)
01
Classes and Objects in Python
02
The init Constructor in Python
03
Instance vs Class Variables in Python
04
Inheritance in Python (Single, Multiple, Multilevel)
05
Polymorphism in Python
06
Encapsulation in Python
07
Abstraction in Python with the abc Module
08
Magic/Dunder Methods in Python
09
Static and Class Methods in Python
10
The @property Decorator in Python
11
Python Dataclasses Explained
Error Handling
01
try, except, finally in Python
02
Handling Multiple Exceptions in Python
03
Raising Custom Exceptions in Python
04
Understanding Python's Exception Hierarchy
05
Assertions in Python
File Handling
01
Reading and Writing Text Files in Python
02
Working with the with Statement in Python
03
Working with CSV Files in Python
04
Working with JSON Files in Python
05
Working with Directories (os, pathlib) in Python
Modules and Packages
01
Importing Built-in Modules in Python
02
Creating Your Own Modules in Python
03
Understanding Packages and init.py
04
Python Standard Library Overview
05
Installing Packages with pip
Advanced Core Concepts
01
Iterators and Iterables in Python
02
Context Managers in Python (with, contextlib)
03
Multithreading Basics in Python
04
Multiprocessing Basics in Python
05
Asyncio Basics: async/await in Python
06
Memory Management and Garbage Collection in Python
07
Type Hints in Python (typing module)

Raising Custom Exceptions in Python

Python ships with more than sixty built-in exception types, but none of them can describe problems specific to your application. python custom exceptions fill that gap — this article covers defining, raising, enriching with extra data, and organizing your own exception types into a coherent hierarchy.

Introduction: Why Build Your Own Exceptions

The limitation of built-in exceptions

Built-in exceptions like ValueError, TypeError, and KeyError cover general-purpose problems well, but they can't express anything specific to your application's actual domain. Consider a banking application: "salary out of an acceptable range" or "account balance too low to complete a withdrawal" are meaningful, specific problems — but there's no LowBalanceError or SalaryNotInRangeError built into Python, because Python has no idea what a "balance" or a "salary" even means in your program.

The benefits of defining your own

Custom exceptions offer two genuine, practical benefits. First, readability: raise LowBalanceError(...) immediately communicates what went wrong, far more clearly than a generic raise ValueError(...) that could mean almost anything depending on where it's caught. Second, precise catching: code that calls your function can catch your specific, meaningful exception type separately from unrelated bugs — except LowBalanceError: catches exactly the situation you defined, without also accidentally swallowing an unrelated ValueError that might indicate a genuine, unrelated programming mistake elsewhere in the same block.

What this article covers

Defining a basic custom exception, raising and catching it, enriching it with custom attributes and behavior, and organizing multiple custom exceptions into a coherent hierarchy — mirroring how Python's own standard library structures its built-in exceptions.

Defining a Basic Custom Exception

The minimal pattern

A custom exception is simply a class that inherits from Exception (or one of its subclasses) — often needing nothing more than pass, or a docstring describing when it's raised:

class InvalidAgeError(Exception):
    """Raised when an age value falls outside a valid range."""
    pass

That's a complete, fully functional custom exception. Inheriting from Exception automatically gives it everything a normal exception needs — the ability to carry a message, be raised, and be caught — without you needing to implement any of that machinery yourself.

Naming convention: the Error suffix

By strong, near-universal convention, custom exceptions are named with an Error suffix — InvalidAgeError, LowBalanceError, ConfigValidationError. This makes them instantly recognizable as exception types at a glance, consistent with the naming pattern used throughout Python's own built-in exceptions (ValueError, TypeError, KeyError, and so on).

Important: always inherit from Exception, never BaseException directly

This is genuinely important guidance, worth internalizing early: always inherit from Exception, never directly from BaseException.

class InvalidAgeError(Exception):   # correct
    pass

class InvalidAgeError(BaseException):   # avoid this
    pass

BaseException sits above Exception in Python's exception hierarchy, and it includes things like KeyboardInterrupt and SystemExit — signals that shouldn't be caught by normal, everyday error-handling code, exactly as covered in the earlier try/except/finally article. Inheriting from BaseException directly means your custom exception could accidentally slip past a standard except Exception: catch-all, behaving inconsistently with every other exception in your program. Exception is, for all practical purposes, always the correct base class for a custom exception.

Raising and Catching the Custom Exception

Basic raise and catch
class InvalidAgeError(Exception):
    pass

def set_age(age):
    if age < 0:
        raise InvalidAgeError("Age cannot be negative")
    return age

try:
    set_age(-5)
except InvalidAgeError as e:
    print(f"Invalid age: {e}")
# Invalid age: Age cannot be negative

raise InvalidAgeError("Age cannot be negative") works exactly like raising any built-in exception — the message passed to the constructor becomes accessible when the exception is caught, printed automatically as part of the exception's string representation.

A practical example: divide()
class DivisionError(Exception):
    """Raised when a division operation cannot be completed safely."""
    pass

def divide(a, b):
    if b == 0:
        raise DivisionError(f"Cannot divide {a} by zero")
    return a / b

try:
    result = divide(10, 0)
except DivisionError as e:
    print(f"Division failed: {e}")

Instead of letting Python's built-in ZeroDivisionError leak through directly, divide() raises its own, more specific DivisionError — giving the caller a type that's meaningful specifically to this function's own domain, rather than a generic error that could originate from anywhere in the standard library.

A brief note on exception chaining with from

When you raise a new exception from inside an except block — a common pattern when translating a low-level error into a more meaningful, higher-level one — Python automatically shows both exceptions in the traceback, but you can make that relationship explicit with raise ... from ...:

class ConfigError(Exception):
    pass

def load_setting(config, key):
    try:
        return config[key]
    except KeyError as e:
        raise ConfigError(f"Missing required setting: {key}") from e

load_setting({}, "api_key")
KeyError: 'api_key'

The above exception was the direct cause of the following exception:

ConfigError: Missing required setting: api_key

from e explicitly marks the original KeyError as the direct cause of the new ConfigError, producing a clearer, more informative traceback than simply raising the new exception on its own — genuinely useful for debugging, since it preserves the original, lower-level error alongside your own more meaningful one, rather than discarding that original context entirely.

Adding Custom Attributes and Overriding init

Overriding init for extra context

Custom exceptions can carry more than just a message — overriding __init__ lets you attach whatever extra context is genuinely useful, like the specific invalid value that triggered the error:

class SalaryNotInRangeError(Exception):
    def __init__(self, salary, min_salary, max_salary):
        self.salary = salary
        self.min_salary = min_salary
        self.max_salary = max_salary
        message = f"Salary {salary} is not within the allowed range ({min_salary}-{max_salary})"
        super().__init__(message)

def set_salary(salary):
    if not (30000 <= salary <= 150000):
        raise SalaryNotInRangeError(salary, 30000, 150000)
    return salary

try:
    set_salary(20000)
except SalaryNotInRangeError as e:
    print(e)                  # Salary 20000 is not within the allowed range (30000-150000)
    print(e.salary)            # 20000 — the actual offending value, accessible directly

Calling super().__init__(message) is important here — it ensures the exception is still fully functional as a normal exception (printable, correctly formatted in tracebacks), exactly the same principle behind calling super().__init__() in any subclass's constructor, covered in the earlier inheritance article. Without that call, str(e) and the default traceback output would be empty or unhelpful, even though e.salary would still technically work.

Overriding str for custom display

You can further customize exactly how the exception displays when printed or logged, using __str__, exactly as covered in the earlier magic methods article:

class LowBalanceError(Exception):
    def __init__(self, balance, required):
        self.balance = balance
        self.required = required
        super().__init__(f"Insufficient balance: have {balance}, need {required}")

    def __str__(self):
        shortfall = self.required - self.balance
        return f"Balance too low by {shortfall} (have {self.balance}, need {self.required})"

try:
    raise LowBalanceError(50, 200)
except LowBalanceError as e:
    print(e)   # Balance too low by 150 (have 50, need 200)

This gives you full control over the exact wording shown when the exception is printed or logged, independent of whatever message was originally passed to super().__init__() — useful when you want the displayed message to include a derived value (like shortfall here) that wasn't part of the original constructor arguments directly.

Building an Exception Hierarchy

A common base class

For any application with more than one or two custom exceptions, it's genuinely worth defining a shared base class that all of your application-specific exceptions inherit from:

class AppError(Exception):
    """Base class for all custom exceptions in this application."""
    pass

class InvalidAgeError(AppError):
    pass

class LowBalanceError(AppError):
    pass

class ConfigError(AppError):
    pass

This mirrors exactly how Python's own standard library organizes its built-in exceptions — FileNotFoundError, PermissionError, and several others all inherit from a common OSError base class, letting code catch broadly (except OSError:) or narrowly (except FileNotFoundError:) depending on what a given situation actually calls for.

Why this matters

With a shared AppError base class in place, calling code gains real flexibility:

try:
    risky_app_operation()
except InvalidAgeError:
    print("Specifically handle invalid age")
except AppError:
    print("Handle any other application-specific error")
except Exception:
    print("Handle a genuinely unexpected error")

This lets you catch a specific exception type when you have a specific, meaningful response to it, while still being able to catch any application-specific error broadly (via AppError) as a fallback — distinctly separate from a final, genuinely broad except Exception: catching truly unexpected bugs that aren't part of your application's defined error vocabulary at all.

Best practices

Keep custom exceptions in a dedicated module for any project of meaningful size — a single exceptions.py file, importable from anywhere else in your codebase:

# exceptions.py
class AppError(Exception):
    """Base class for all custom exceptions in this application."""
    pass

class InvalidAgeError(AppError):
    """Raised when a provided age value falls outside the valid range."""
    pass

class LowBalanceError(AppError):
    """Raised when an account balance is insufficient for a requested operation."""
    pass
# elsewhere in your codebase
from exceptions import InvalidAgeError, LowBalanceError

Centralizing every custom exception in one dedicated module makes them easy to find, easy to import consistently, and gives you one clear place to see your entire application's defined error vocabulary at a glance.

Document each exception's docstring, explaining exactly when it's meant to be raised. A one-line docstring, exactly as shown above, costs almost nothing to write and saves real time for anyone (including future you) trying to understand exactly what condition a given exception represents, without needing to hunt down and read every place it's actually raised throughout the codebase.

PREVIOUSNEXT LESSON