English · Русский
Typed HTTP errors for FastAPI — exact Literal codes in OpenAPI, discriminated oneOf unions on the code field, and a single source of truth: the error class itself.
In plain FastAPI, raise HTTPException(404, "...") buries the error code in a string — clients can't switch on it, OpenAPI has no code type or body schema, and you hand-maintain responses={} on every route. This package makes an error a class: its HTTP status, machine-readable code and response model are declared once and derived automatically, so your error contract is typed, self-documenting, and verifiable in CI.
class NotFoundError(BaseError[Literal[ErrorCode.NOT_FOUND]]): http_status = HTTPStatus.NOT_FOUND @router.get("/items/{item_id}") def get_item(item_id: int) -> Annotated[Item, Raises[NotFoundError]]: if item_id == 0: raise NotFoundError("No item") return Item(item_id=item_id)
The response body is always {"code": "NOT_FOUND", "detail": "No item"}, and OpenAPI gets a 404 with the exact Literal["NOT_FOUND"] code and body model — no manual responses.
pip install fastapi-typed-errors # core + decorator pip install "fastapi-typed-errors[cli]" # + the CI-checker CLI
Requires Python 3.12+, FastAPI ≥ 0.115, Pydantic ≥ 2.9.
1. Define errors and register the one handler.
from fastapi import FastAPI from fastapi_typed_errors import BaseError, handle_base_error app = FastAPI() app.add_exception_handler(BaseError, handle_base_error)
2. Declare errors — pick your level of magic:
# a) by hand (core only) — write responses yourself @app.get("/x", responses={404: {"model": error_models(NotFoundError)}}) def a() -> Item: ... # b) declared — the marker fills responses for you router = with_errors(APIRouter()) @router.get("/y") def b() -> Annotated[Item, Raises[NotFoundError, ForbiddenError]]: ... # c) automatic — no markers at all; errors are found statically router = with_errors(APIRouter(), auto=True) @router.get("/z") def c(user: Annotated[User, Depends(current_user)]) -> Item: raise NotFoundError("...") # auto -> 404, plus whatever current_user raises
3. Verify the contract in CI. check_raises compares what each route declares against what it can actually raise — in the endpoint and its whole dependency tree:
def test_error_contracts() -> None: assert check_raises(app).ok
Or as a command: fastapi-typed-errors check app.main:app (exit 0/1/2).
- Exact types in OpenAPI — a precise
Literalcode per status; several errors on one status become a discriminatedoneOfunion, so Swagger UI shows a variant picker by code. - Single source of truth — status, code and model declared once; the metaclass derives the rest.
- Zero-boilerplate
responses— via theRaisesmarker or fully automaticauto=True. - Static contract checking —
check_raisescatches a raise you forgot to declare (or a dead declaration) before it ships. - Non-invasive —
with_errorspatches the router in place and preserves object identity, soinclude_router, websockets and app-level decorators keep working natively. - Rigorous — fully typed (
py.typed), 100% branch-covered, checked withruff+tyat max strictness.
📖 Full documentation — guide, customization (custom envelopes, ABC, bare-string codes), limitations, and an auto-generated API reference. Available in English and Russian.
MIT.