Learn to raise exceptions and create a Python custom exception in your own code. Build clear error names and tidy error families that make debugging easier, with tested examples and real-world patterns.
“The best error message is the one that never shows up.”
Thomas Fuchs
Last Updated: July 2026 | Tested on: Python 3.14.6 | Difficulty: Beginner | Reading Time: 16 minutes
Python ships with built-in errors like ValueError and FileNotFoundError, and they cover the everyday stuff. But real apps hit problems that those generic names cannot describe well. A banking app that runs out of money needs an InsufficientFundsError, not a vague ValueError. A Python custom exception lets you give an error a name that says exactly what went wrong, so your code reads like plain English and your error handling stays precise.
Making one is easy: you write a small class that inherits from Exception, and that is it. Making it genuinely useful takes a bit more: you add context, group related errors into a family, and follow a few patterns that turn a crash into a clue. This post walks through all of it with examples you can paste straight into your own projects.
In the exception handling tutorial, you learned to catch exceptions. Now it is time to raise them. Say a user named Vinay types a negative number as his age, or another user, Aditi, sends an empty username to your API (Application Programming Interface). Your function should stop and complain loudly. It should not quietly hand back garbage. You raise exceptions to enforce the rules your code depends on.
Think of a smoke alarm. The moment something is wrong, it goes off, loud and clear, so you act before the small problem becomes a big one. Raising an exception is your function pulling that alarm. A custom exception is just a labelled alarm, so instead of a generic beep you get “kitchen smoke” or “low battery” and you know exactly where to look.
Table of Contents
Raising Exceptions with raise
Tip: click the diagram to open it full screen, then use the zoom and pan controls for a closer look.
The diagram shows Python’s built-in exception family tree. At the very top sits BaseException. Below it the tree splits into two sides: Exception, which holds the errors you normally catch, and the system-level ones like KeyboardInterrupt that you should leave alone. Every Python custom exception you write inherits from Exception (or one of its children), which is exactly why a plain except Exception handler scoops it up for free. Where you hook your class into this tree decides which except clauses will catch it.
📄 raise_basics.py: raising built-in exceptions
def set_age(name, age):
if not isinstance(age, int):
raise TypeError(f"Age must be an integer, got {type(age).__name__}")
if age < 0:
raise ValueError(f"Age cannot be negative, got {age}")
if age > 150:
raise ValueError(f"Age {age} is unrealistic")
return {"name": name, "age": age}
# Valid
print(set_age("Rahul", 28))
# Invalid, caught
try:
set_age("Vinay", -5)
except ValueError as e:
print(f"Error: {e}")
try:
set_age("Pravin", "twenty")
except TypeError as e:
print(f"Error: {e}")
▶ Output
{'name': 'Rahul', 'age': 28}
Error: Age cannot be negative, got -5
Error: Age must be an integer, got str
What happened here: raise ValueError("message") builds a ValueError object and stops the function dead in its tracks. If nothing catches it with try/except, the program crashes and prints a traceback. Notice the messages: each one names the bad value (got -5, got str). That tiny habit saves you minutes of guessing later, because the error tells you what it choked on, not just that it choked.
When to Raise Exceptions
Raise an exception when your function gets input it simply cannot work with. Think of a security guard at an office gate: if your visitor pass is invalid, the guard stops you right at the entrance instead of letting you wander around and cause confusion on the fifth floor. Here is the rule of thumb: if the function cannot do what its name promises, raise. set_age() cannot set an age of -5, so it refuses and raises instead of pretending everything is fine.
📄 when_to_raise.py: guard clauses
def calculate_bmi(weight_kg, height_m):
"""Calculate Body Mass Index."""
if weight_kg <= 0:
raise ValueError(f"Weight must be positive, got {weight_kg}")
if height_m <= 0:
raise ValueError(f"Height must be positive, got {height_m}")
if height_m > 3.0:
raise ValueError(f"Height {height_m}m seems wrong, did you pass cm?")
bmi = weight_kg / (height_m ** 2)
return round(bmi, 1)
# Valid
print(f"BMI: {calculate_bmi(75, 1.78)}")
# Catches the "probably meant cm" guard
try:
calculate_bmi(75, 178) # Forgot to convert cm to m
except ValueError as e:
print(f"Error: {e}")
▶ Output
BMI: 23.7 Error: Height 178m seems wrong, did you pass cm?
What happened here: those if checks at the top are called guard clauses. They catch bad input before any real work starts. Passing 178 as a height in metres is almost certainly a centimetres mix-up, so the function says so out loud instead of returning a nonsense BMI (Body Mass Index). Failing fast with a clear message beats silently computing a wrong answer that nobody notices until it is in production.
Re-raising Exceptions
📄 reraise.py: log, then re-raise
def process_payment(amount):
try:
# Simulate payment processing
if amount > 100000:
raise RuntimeError("Payment gateway timeout")
return f"Payment of ₹{amount} processed"
except RuntimeError:
print(f" [LOG] Payment failed for ₹{amount}, passing error up...")
raise # Re-raise the same exception to the caller
try:
result = process_payment(200000)
except RuntimeError as e:
print(f" [CALLER] Caught: {e}")
▶ Output
[LOG] Payment failed for ₹200000, passing error up... [CALLER] Caught: Payment gateway timeout
What happened here: a bare raise with nothing after it re-throws the exception you are currently handling, untouched. So you get the best of both worlds: the function logs the failure on its way out, then passes the same error up to the caller to deal with. Think of it like signing for a parcel and then handing it on. You noted that it arrived, but the package keeps moving to whoever it was meant for.
Creating Custom Exceptions
📄 custom_exception.py: your first custom exception
class InsufficientFundsError(Exception):
"""Raised when a withdrawal exceeds the account balance."""
pass
class InvalidAmountError(Exception):
"""Raised when a transaction amount is invalid."""
pass
class BankAccount:
def __init__(self, owner, balance=0):
self.owner = owner
self.balance = balance
def withdraw(self, amount):
if amount <= 0:
raise InvalidAmountError(f"Amount must be positive, got {amount}")
if amount > self.balance:
raise InsufficientFundsError(
f"Cannot withdraw ₹{amount}, balance is ₹{self.balance}"
)
self.balance -= amount
return self.balance
account = BankAccount("Anvay", 5000)
try:
account.withdraw(3000)
print(f"Balance: ₹{account.balance}")
account.withdraw(5000) # More than remaining balance
except InsufficientFundsError as e:
print(f"Insufficient funds: {e}")
except InvalidAmountError as e:
print(f"Invalid amount: {e}")
▶ Output
Balance: ₹2000 Insufficient funds: Cannot withdraw ₹5000, balance is ₹2000
What happened here: a custom exception is just a class that inherits from Exception. The simplest version has nothing but pass in the body, and that is already useful. The real win is the name. InsufficientFundsError tells you what went wrong at a glance, before you even read the message. It is the difference between a fire alarm that just beeps and one that flashes the word “kitchen” at you.
Custom Exception with Extra Data
A plain text message is fine for a human reading the screen, but code that catches the error often wants the details in neat, separate pieces: which field failed, what value it got, what the problem was. You can carry all of that on the exception itself by writing a small __init__. Think of it like a hospital wristband. The error is the patient, and the wristband holds the name, the ward, and the reason for the visit, all in labelled boxes instead of one scribbled note.
📄 exception_data.py: attach structured data to an exception
class ValidationError(Exception):
"""Exception with field name and details attached."""
def __init__(self, field, value, message):
self.field = field
self.value = value
self.message = message
super().__init__(f"{field}: {message} (got {value!r})")
def validate_user(data):
errors = []
if not data.get("name"):
errors.append(ValidationError("name", data.get("name"), "cannot be empty"))
if not isinstance(data.get("age"), int) or data.get("age", 0) < 0:
errors.append(ValidationError("age", data.get("age"), "must be a positive integer"))
if errors:
raise errors[0] # Raise first error (or collect all)
return True
# Test validation
test_data = {"name": "", "age": -5}
try:
validate_user(test_data)
except ValidationError as e:
print(f"Field: {e.field}")
print(f"Value: {e.value!r}")
print(f"Message: {e.message}")
print(f"Full: {e}")
▶ Output
Field: name Value: '' Message: cannot be empty Full: name: cannot be empty (got '')
What happened here: the custom __init__ stores field, value, and message on the exception, then calls super().__init__() to set the human-readable text. Now the handler can read e.field and e.value directly. No fragile string parsing, no splitting the message on colons and hoping. The data is right there, ready to log, show in a form, or send back as JSON (JavaScript Object Notation).
Exception Hierarchy Pattern
Here is the pattern that real libraries lean on. You make one base exception for your whole app, say AppError, and then every specific error inherits from it. It is like a family surname. AuthenticationError, AuthorizationError, and NotFoundError are all “AppErrors” the same way three siblings share a last name. That one shared parent gives callers a choice: catch AppError to handle anything your app throws, or catch one specific child when you want to react to just that case.
📄 hierarchy.py: a base exception with specific subtypes
# Base exception for the entire application
class AppError(Exception):
"""Base exception for the TechnoScripts app."""
pass
# Specific exception categories
class AuthenticationError(AppError):
"""Login or session failures."""
pass
class AuthorizationError(AppError):
"""Permission denied."""
pass
class NotFoundError(AppError):
"""Resource not found."""
pass
# Usage: callers can catch broadly or specifically
def get_user(user_id):
if user_id == 0:
raise AuthenticationError("Invalid session")
if user_id < 0:
raise AuthorizationError(f"User {user_id} cannot access this resource")
if user_id > 100:
raise NotFoundError(f"User {user_id} not found")
return {"id": user_id, "name": "Prathamesh"}
# One except clause catches every app error
for uid in [0, -7, 200, 1]:
try:
user = get_user(uid)
print(f" Found: {user}")
except AppError as e:
# Catches AuthenticationError, AuthorizationError, NotFoundError
print(f" {type(e).__name__}: {e}")
▶ Output
AuthenticationError: Invalid session
AuthorizationError: User -7 cannot access this resource
NotFoundError: User 200 not found
Found: {'id': 1, 'name': 'Prathamesh'}
What happened here: the loop has a single except AppError, yet it cleanly handles three different errors, because all three inherit from AppError. That is the payoff of the family tree. Catch AppError when you just want to mop up anything your app can throw. Catch AuthenticationError on its own when you want to bounce the user to a login page. Django, Flask, and pretty much every serious Python library are built on this exact pattern.
Exception Chaining
Sometimes a low-level error bubbles up that means nothing to your user. A raw FileNotFoundError on a config path is not helpful to the person running your app. You want to show them a clean ConfigError instead. But if you just swallow the original error, you throw away the clue that says why it broke. raise X from Y solves this. It shows your tidy error on top while keeping the original cause attached underneath, like stapling the original receipt to a polished summary report.
📄 chaining.py: raise … from … keeps the original cause
import json
class ConfigError(Exception):
pass
def load_config(path):
try:
with open(path) as f:
return json.load(f)
except (FileNotFoundError, json.JSONDecodeError) as original:
raise ConfigError(f"Failed to load config from {path}") from original
try:
config = load_config("nonexistent.json")
except ConfigError as e:
print(f"Config error: {e}")
print(f"Caused by: {e.__cause__}")
▶ Output
Config error: Failed to load config from nonexistent.json Caused by: [Errno 2] No such file or directory: 'nonexistent.json'
What happened here: raise X from Y chained the two errors together. The caller sees your friendly ConfigError, while e.__cause__ still holds the original FileNotFoundError for debugging. In a full traceback this is the part that prints “The above exception was the direct cause of the following exception”. Your API stays clean, and the trail back to the real problem stays intact.
Real-World Pattern: Validation Library
Here is a pattern you will use constantly: checking a whole form at once. If you stop at the first bad field, the user fixes it, hits submit, and gets hit with the next error, one annoying round trip at a time. Better to gather every problem and report them together. A custom exception that carries a list of errors does this neatly. Picture a teacher grading a test: instead of handing the paper back at the first wrong answer, they mark every mistake and return it once, so you can fix them all in one go.
📄 validation.py: collecting multiple validation errors at once
class ValidationErrors(Exception):
"""Collects multiple validation errors."""
def __init__(self, errors):
self.errors = errors
super().__init__(f"{len(errors)} validation error(s)")
def __str__(self):
lines = [f"{len(self.errors)} validation error(s):"]
for err in self.errors:
lines.append(f" - {err}")
return "\n".join(lines)
def validate_registration(data):
errors = []
if not data.get("username"):
errors.append("username is required")
elif len(data["username"]) < 3:
errors.append("username must be at least 3 characters")
if not data.get("email") or "@" not in data.get("email", ""):
errors.append("valid email is required")
if not data.get("password") or len(data.get("password", "")) < 8:
errors.append("password must be at least 8 characters")
if errors:
raise ValidationErrors(errors)
return True
# Test with bad data
try:
validate_registration({"username": "ab", "email": "bad", "password": "123"})
except ValidationErrors as e:
print(e)
print(f"\nError count: {len(e.errors)}")
▶ Output
3 validation error(s): - username must be at least 3 characters - valid email is required - password must be at least 8 characters Error count: 3
What happened here: the function collected three failures into a list, then raised one ValidationErrors that carries all of them. The custom __str__ formats the list into a readable block, and e.errors still hands the caller the raw list if it wants to render each one next to its form field. One exception, every problem, no back-and-forth.
Common Mistakes
Mistake 1: Inheriting from BaseException instead of Exception
Always inherit from Exception, never from BaseException. BaseException sits above SystemExit and KeyboardInterrupt, the signals that let a user quit your program with Ctrl+C. If your error lived up there, a plain except Exception would miss it, and a careless catch-all could swallow the very thing that lets people stop the program. Keep your exceptions where they belong, one level down, under Exception.
Mistake 2: Raising strings instead of exception objects
📄 mistake_string.py
# BAD: this is a Python 2 habit, it does not work in Python 3
# raise "Something went wrong" # TypeError!
# GOOD: raise an actual exception object
raise ValueError("Something went wrong")
Mistake 3: Creating too many custom exceptions
You do not need a brand new exception class for every little thing. If a built-in like ValueError, TypeError, or KeyError already describes the problem, just use it. Save your custom exceptions for the errors that are special to your domain, the ones a built-in name cannot capture, such as InsufficientFundsError or ConfigError. A wall of one-off exception classes is as hard to read as no names at all.
Best Practices
- DO use built-in exceptions when they fit (
ValueError,TypeError,KeyError) - DO inherit custom exceptions from
Exception, notBaseException - DO create a base exception for your app/library (
AppError) with specific subtypes - DO include the bad value in the error message
- DO use
raise X from Yto chain exceptions and preserve the cause - DON’T create a custom exception when
ValueErrorsays it all
Conclusion
raise lets your own code signal that something is wrong. A Python custom exception gives that signal a clear name and, when you need it, structured data you can read back later. Group related errors under one base class so callers can catch broadly or pick out a single case. Reach for raise ... from ... when you want a clean error on top and the original cause still attached underneath. These are the exact patterns the big Python libraries use to talk about errors, and now they are yours too.
Next up is Python Project: Build an Expense Tracker (Files, JSON, Dicts), where you put these error-handling habits to work in a real app that reads and writes its own data. And if you want to see everything this series covers, from first steps to AI/ML, browse the Python + AI/ML tutorial series home.
Practice Exercises
- Exercise 1: Create
NegativeNumberErrorwith the invalid value. - Exercise 2: Build a validation hierarchy: ValidationError with Type, Range, Format subclasses.
- Exercise 3: Build a context-aware exception capturing stack, timestamp, input with
to_dict().
Frequently Asked Questions
How do I create a custom exception in Python?
Create a class that inherits from Exception: class MyError(Exception): pass. Add __init__ if you need to attach extra data. Always inherit from Exception, not BaseException.
When should I raise an exception vs return None?
Raise an exception when the function cannot do what its name promises, for example the caller passed bad input or a needed file is missing. Return None when “nothing found” is a normal, expected outcome rather than an error.
What is the difference between raise and raise from?
raise X raises exception X. raise X from Y raises X and records Y as the cause, preserving the diagnostic chain. The traceback shows “The above exception was the direct cause of…”
Should custom exceptions inherit from Exception or BaseException?
Always from Exception. BaseException is for system-level exceptions like SystemExit and KeyboardInterrupt that should NOT be caught by normal except Exception: handlers.
Can I raise built-in exceptions from my code?
Yes, and you should when they fit. raise ValueError('age must be positive') is perfectly fine. Only create custom exceptions when built-in names don’t accurately describe your domain-specific error.
Interview Questions on Python Custom Exceptions
The same ideas as they show up in real interviews, framed as scenarios you can practice out loud.
Q: What does a bare raise statement (with nothing after it) do, and where can you use it?
A bare raise re-raises the exception currently being handled, with its original traceback intact. It only works inside an except block (or code called from one); anywhere else it raises a RuntimeError because there is no active exception. It is the standard way to log or clean up on the way out while still letting the caller see the original error.
Q: Why should a custom exception’s __init__ usually call super().__init__()?
Calling super().__init__(message) stores the message on the exception so that str(e), repr(e), and the printed traceback all show something useful. If you skip it and only set your own attributes, printing the exception can give you an empty string, which makes logs and tracebacks much harder to read. You still attach your extra data (like field or value) as normal attributes alongside it.
Q: In a try block with multiple except clauses, does the order of except AppError and except NotFoundError matter when NotFoundError inherits from AppError?
Yes, order matters because Python checks except clauses top to bottom and runs the first one that matches. If except AppError comes first, it also matches every subclass, so the except NotFoundError below it becomes dead code that never runs. Always put the most specific exception first and the broad parent last.
Q: You wrapped a low-level database error in your own ConfigError, but the traceback no longer shows the original error. What likely happened and how do you fix it?
Someone probably wrote raise ConfigError(...) from None, which deliberately suppresses the original exception’s context, or they caught the error and raised a new one in a way that dropped the useful details. The fix is to chain explicitly: raise ConfigError(...) from original, which stores the low-level error in __cause__ and prints it in the traceback. Reserve from None for cases where the inner error is genuinely noise for the caller.
Q: Your team’s library raises a plain Exception("something failed") for every failure, and callers complain they cannot handle different errors differently. What do you refactor first?
Introduce a small hierarchy: one base class like LibraryError(Exception), then specific subclasses such as ConnectionError-style or ValidationError-style children for each failure category. Change the raise sites to use the specific classes, keeping messages intact. Callers can then catch the base class for a blanket handler or a specific subclass for targeted recovery, and existing except Exception code keeps working during the migration.
Q: A user presses Ctrl+C to stop your script, but it keeps running because a loop has try: ... except BaseException: pass. Why does this happen and what is the correct handler?
Ctrl+C raises KeyboardInterrupt, which inherits from BaseException, so the catch-all swallows it and the loop never stops. The correct pattern is except Exception, which catches ordinary errors but lets KeyboardInterrupt and SystemExit pass through so the program can actually exit. This is also exactly why your own exception classes must inherit from Exception, not BaseException.
Go deeper: when you outgrow this post, the official Python documentation is the next stop.
Related Posts
Previous: Python: Exception Handling, Reading Tracebacks & try/except/else/finally
Next: Python Project: Build an Expense Tracker (Files, JSON, Dicts)
Series Home: Python + AI/ML Tutorial Series

No comment