Skip to content

The exception hierarchy has no stated rule and no docs page #125

Open

Description

@cdwilson

The exception hierarchy has no stated rule and no documentation page.

What happens

Two axes are applied across the error surface, and neither is written down in one place:

  • TinydanticUserError (misuse) vs plain TinydanticError (runtime condition). DocumentIDUpdateError and UnknownUpdateFieldError are user errors; DocumentIDRequiredError — calling replace() on an instance that was never inserted, which is plainly misuse — is not.
  • Multiple inheritance from builtins. DocumentAlreadyExistsError is a ValueError, SelectorError is a ValueError, QueryTypeError is a TypeError, QueryFieldError is both ValueError and AttributeError — but DocumentNotFoundError is not a LookupError/KeyError, and DatabaseNotBoundError is not a RuntimeError.

Each individual choice is defended in its own class docstring. dacac70 even recorded why one of them is a ValueError — but in that class's own docstring, which is exactly the per-class defense that does not add up to a predictable surface. src/tinydantic/_errors.py's module docstring is still one line ("Errors specific to tinydantic."), and there is no errors section anywhere in docs/.

Why it matters

Users cannot predict what to catch. The library's own changelog claims a curated exception surface — "no raw TinyDB or bare built-in exception leaks through tinydantic's public API" — which is a promise about the surface as a whole, so the surface as a whole needs a stated rule.

What to do

A short "Errors" section in the usage docs with:

  1. The hierarchy as a tree.
  2. The rule, stated once — something like: "user errors subclass TinydanticUserError; an error that replaces a builtin also subclasses that builtin, so handlers written against the builtin keep working."
  3. Then apply the rule and fix the inconsistencies it exposes (DocumentIDRequiredError, DocumentNotFoundError, DatabaseNotBoundError are the candidates above).

8648e34 already reclassified the query errors along exactly these lines — QueryConditionError/FindQueryError/SortFieldError became QueryTypeError/QueryValueError/QueryFieldError/QueryUsageError, each carrying the builtin base Python itself would use. That is the rule; it just was not generalized to the rest of the surface or written down.

Also fix here: DocumentAlreadyExistsError can render a malformed message

DocumentAlreadyExistsError.__init__ joins doc_ids into the message with no empty-list guard:

ids = ", ".join(str(doc_id) for doc_id in doc_ids)
noun = "id" if len(doc_ids) == 1 else "ids"
f"Document with {noun} {ids} already exists in table ..."

If doc_ids is empty the message renders as Document with ids already exists in table ... — two spaces, no ids. In insert_many() that is reachable when TinyDB raises ValueError and no document in the batch had a preset id. Only TinyDB's ValueError('Document is not a Mapping') gets there, which serialized documents cannot trigger, so this is defensive-path polish rather than a live bug — but it belongs with the errors pass.

Reproduction

The two axes, applied inconsistently:

import tinydantic as td

for exc in (
    td.DocumentIDUpdateError,
    td.DocumentIDRequiredError,
    td.DocumentAlreadyExistsError,
    td.DocumentNotFoundError,
    td.DatabaseNotBoundError,
):
    bases = [b.__name__ for b in exc.__mro__[1:] if b is not object]
    print(f"{exc.__name__:28} {bases}")

Actual output:

DocumentIDUpdateError        ['TinydanticUserError', 'TinydanticError', 'Exception', 'BaseException']
DocumentIDRequiredError      ['TinydanticError', 'Exception', 'BaseException']
DocumentAlreadyExistsError   ['TinydanticError', 'ValueError', 'Exception', 'BaseException']
DocumentNotFoundError        ['TinydanticError', 'Exception', 'BaseException']
DatabaseNotBoundError        ['TinydanticUserError', 'TinydanticError', 'Exception', 'BaseException']

Expected: one stated rule applied throughout. DocumentIDRequiredError is misuse and should sit under TinydanticUserError alongside DocumentIDUpdateError; DocumentNotFoundError replaces a builtin lookup failure and should carry LookupError the way DocumentAlreadyExistsError carries ValueError.

The malformed message:

from tinydantic import DocumentAlreadyExistsError

print(DocumentAlreadyExistsError(model_name="Book", table_name="book", doc_ids=[]))

Actual output — note the doubled space where the ids should be:

Document with ids  already exists in table 'book' (model 'Book'). Omit id to let TinyDB assign one, or use save()/upsert() to update the existing document.

Expected: a message that reads correctly with no ids to name.

Not verified: the empty-doc_ids path is constructed directly here rather than reached through insert_many(). As the body says, only TinyDB's ValueError('Document is not a Mapping') reaches it and serialized documents cannot trigger that, so this is defensive-path polish, not a live bug.

Environment: tinydantic 0.4.0 at 4d373d1, Python 3.10.20, TinyDB 4.9.0, pydantic 2.13.4, macOS. Run 2026-08-13.


Verified at e7d0ed0 on 2026-08-12. Originally findings L2 and L5 of the 2026-08-06 internal review.

Assisted-by: Claude:claude-opus-5

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    ai-assistedCreated or investigated with help from an AI agentapi-designShape of the public surfacedocumentationImprovements or additions to documentation

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions