Repository navigation
Description
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 plainTinydanticError(runtime condition).DocumentIDUpdateErrorandUnknownUpdateFieldErrorare user errors;DocumentIDRequiredError— callingreplace()on an instance that was never inserted, which is plainly misuse — is not.- Multiple inheritance from builtins.
DocumentAlreadyExistsErroris aValueError,SelectorErroris aValueError,QueryTypeErroris aTypeError,QueryFieldErroris bothValueErrorandAttributeError— butDocumentNotFoundErroris not aLookupError/KeyError, andDatabaseNotBoundErroris not aRuntimeError.
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:
- The hierarchy as a tree.
- 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." - Then apply the rule and fix the inconsistencies it exposes (
DocumentIDRequiredError,DocumentNotFoundError,DatabaseNotBoundErrorare 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
Activity