Course Content
Python Essentials for AI Engineer
6 sections · 48 lessons
What are Custom Exceptions?
What you need to know
The minimum
1class EmptyIndexError(Exception):2 """The vector index has no documents to search."""34try:5 raise EmptyIndexError("index 'faq-v3' has 0 documents")6except EmptyIndexError as e:7 print(e) # index 'faq-v3' has 0 documentsA class body with just a docstring is enough. Names end in Error by convention.
Adding data
When the caller needs details to make a decision, store them as attributes and still pass a readable message to the parent:
1class RateLimitError(Exception):2 def __init__(self, message, retry_after):3 super().__init__(message)4 self.retry_after = retry_after56try:7 raise RateLimitError("429 from provider", retry_after=12)8except RateLimitError as e:9 print(e, "| wait", e.retry_after, "s") # 429 from provider | wait 12 sA small hierarchy
Libraries define one base exception and a few subclasses: requests has RequestException, and the major LLM SDKs expose classes such as RateLimitError, APITimeoutError and BadRequestError under a common base. Callers can catch one specific case or the whole family.
When not to create one
If a built-in already says it — a bad argument value is a ValueError — use the built-in. Create custom classes for failures callers will want to handle differently, not one per function.
A real-life example
A team wraps several LLM providers behind one client. Callers should not care which provider failed; they need to know what kind of failure it was, because each needs a different fix:
1class LLMError(Exception):2 """Base class for every failure from our LLM layer."""34class RateLimitError(LLMError):5 def __init__(self, retry_after):6 super().__init__(f"rate limited, retry after {retry_after}s")7 self.retry_after = retry_after89class ContextLengthError(LLMError):10 def __init__(self, tokens, limit):11 super().__init__(f"{tokens} tokens exceeds limit of {limit}")12 self.tokens, self.limit = tokens, limit1314class ContentFilterError(LLMError):15 pass1617def handle(err):18 if isinstance(err, RateLimitError):19 return f"sleep {err.retry_after}s and retry"20 if isinstance(err, ContextLengthError):21 return f"drop {err.tokens - err.limit} tokens of history and retry"22 if isinstance(err, LLMError):23 return "show a polite fallback message"2425for err in [RateLimitError(12), ContextLengthError(9200, 8192), ContentFilterError("blocked")]:26 try:27 raise err28 except LLMError as e:29 print(f"{type(e).__name__}: {handle(e)}")30# RateLimitError: sleep 12s and retry31# ContextLengthError: drop 1008 tokens of history and retry32# ContentFilterError: show a polite fallback messageEach provider adapter translates its own HTTP errors into these classes. When the team adds a new provider, the retry, truncation and fallback logic does not change. And because everything inherits from LLMError, the web layer can catch LLMError once and never accidentally hide a genuine bug like a KeyError.
Follow-up questions to expect
- "Why inherit from
Exceptionand notBaseException?" —BaseExceptionis for system-exiting events likeKeyboardInterrupt. Code that doesexcept Exceptionwould not catch your error. - "Should custom exceptions carry extra data?" — Yes, when the caller needs it to decide what to do, such as
retry_afterorstatus_code. Always pass a readable message tosuper().__init__. - "How many custom exceptions should a project have?" — Few: one base class per library or layer, plus a subclass for each failure that callers handle differently.