python-custom-exceptions: Structured, Semantic Exceptions for Python
python-custom-exceptions replaces vague bare exceptions with structured, self-describing classes that carry diagnostic context and serialize to JSON — making production failures actually debuggable.
It’s 3am. Something failed in production. You open the logs and find:
ValueError: something went wrong
No context. No subject. No idea which part of the system raised it or what state it was in. You start adding print statements and re-deploying.
This is the problem python-custom-exceptions solves.
Why I Built This
The standard Python exception hierarchy is fine for stdlib errors, but in application code you end up with one of two patterns: either you raise bare built-ins (ValueError, RuntimeError) with a string message that loses all structure once it hits logs, or you define a dozen one-off exception classes per project — each one slightly different, none of them consistent.
I wanted a third option: a small set of semantic, reusable exception classes that carry structured context and produce readable, parseable output everywhere they surface. Built once, used across every project.
Installation
pip install python-custom-exceptions
uv add python-custom-exceptions
The Foundation: BaseCustomException + DiagnosticInfo
Every exception in the library inherits from BaseCustomException. The key idea is that every exception carries two things: a message and a diagnostic_info dict — context that travels with the exception and appears in the output automatically.
from python_custom_exceptions import BaseCustomException, DiagnosticInfo
class DeploymentDiagnostic(DiagnosticInfo):
service: str
environment: str
replica_count: int
raise BaseCustomException(
message_with_marking_dynamic_variables="Deployment health check failed",
diagnostic_info=DeploymentDiagnostic(
service="api",
environment="production",
replica_count=0
)
)
When this exception is caught and logged, str(exc) produces structured JSON:
{
"exception_type": "BaseCustomException",
"message": "Deployment health check failed",
"diagnostic_info": {
"service": "api",
"environment": "production",
"replica_count": 0
}
}
Every exception is self-describing. No more guessing what state the system was in.
Ready-to-Use Exceptions
The library ships with semantic classes that cover the cases that come up repeatedly in infrastructure and backend code. You stop asking “what should I call this exception?” and start picking the right one.
Existence checks — for anything that should or shouldn’t exist:
from python_custom_exceptions import IsNotExistException, IsExistAfterOperationException
raise IsNotExistException(subject="Config file", diagnostic_info={"path": "/etc/app/config.yml"})
raise IsExistAfterOperationException(subject="Lock file", operation="cleanup")
Connections — with a default message that tells you exactly what to check:
from python_custom_exceptions import ConnectionException
raise ConnectionException(host="redis://cache:6379")
# "Connection failed: Unable to establish a connection to the remote host ["redis://cache:6379"].
# Please check your credentials, network status, and host availability."
Operations — for things that failed, weren’t operational, or unexpectedly succeeded:
from python_custom_exceptions import OperationException, NotOperationalException
raise OperationException(operation="database migration")
raise NotOperationalException(subject="Message queue")
Timeouts, unsupported cases, wrong usage — each with a clear, consistent message format:
from python_custom_exceptions import TimedExecutionException, UnsupportedExceptions, WrongUsageException
raise TimedExecutionException(operation="pod readiness check", timeout=120)
raise UnsupportedExceptions(unsupported_subject="Windows platform")
raise WrongUsageException(message="call connect() before query()")
The Difference in Practice
Before:
raise ValueError(f"connection to {host} failed")
# → logs: ValueError: connection to redis://cache:6379 failed
After:
raise ConnectionException(host=host, diagnostic_info={"attempt": 3, "timeout": 30})
# → logs: {"exception_type": "ConnectionException", "message": "...", "diagnostic_info": {...}}
The second version is greppable, parseable by log aggregators, and tells you everything you need to know — without adding a single extra line of context code.
Links
If you’ve ever stared at a bare RuntimeError in a production log at 3am, this is the package that makes sure it never happens again.
Comments