Skip to content

tinydantic ¤

A simple Python object-document mapper (ODM) for TinyDB.

tinydantic maps Python objects to and from documents stored in the TinyDB document database.

Attributes:

Name Type Description
__version__ str

The tinydantic package version.

AmbiguousConfigError ¤

Bases: TinydanticUserError

Conflicting tinydantic config inherited from unrelated bases.

Raised at class-definition time when two base classes that are not part of one inheritance chain supply different values for the same tinydantic config key and the new class does not set that key itself. tinydantic refuses to guess which base should win — see the tinydantic._config module docstring for the design rationale.

Source code in src/tinydantic/_errors.py
class AmbiguousConfigError(TinydanticUserError):
    """Conflicting tinydantic config inherited from unrelated bases.

    Raised at class-definition time when two base classes that are not
    part of one inheritance chain supply different values for the same
    tinydantic config key and the new class does not set that key
    itself. tinydantic refuses to guess which base should win — see
    the ``tinydantic._config`` module docstring for the design
    rationale.
    """

    def __init__(
        self,
        *,
        model_name: str,
        key: str,
        first: str,
        second: str,
    ) -> None:
        """Initialize with the conflicting classes and config key."""
        super().__init__(
            f"{model_name!r} inherits conflicting values for tinydantic "
            f"config key {key!r} from unrelated base classes {first!r} "
            f"and {second!r}. Set {key}= explicitly on {model_name!r} "
            "to resolve the ambiguity.",
        )

__init__ ¤

__init__(
    *, model_name: str, key: str, first: str, second: str
) -> None

Initialize with the conflicting classes and config key.

Source code in src/tinydantic/_errors.py
def __init__(
    self,
    *,
    model_name: str,
    key: str,
    first: str,
    second: str,
) -> None:
    """Initialize with the conflicting classes and config key."""
    super().__init__(
        f"{model_name!r} inherits conflicting values for tinydantic "
        f"config key {key!r} from unrelated base classes {first!r} "
        f"and {second!r}. Set {key}= explicitly on {model_name!r} "
        "to resolve the ambiguity.",
    )

DatabaseNotBoundError ¤

Bases: TinydanticUserError

No database is bound to the model class.

Raised when a table operation is attempted on a model that has no database configured anywhere in its class hierarchy.

Source code in src/tinydantic/_errors.py
class DatabaseNotBoundError(TinydanticUserError):
    """No database is bound to the model class.

    Raised when a table operation is attempted on a model that has no
    ``database`` configured anywhere in its class hierarchy.
    """

    def __init__(self, model_name: str) -> None:
        """Initialize with the name of the unbound model class."""
        super().__init__(
            f"No database is bound to {model_name!r}. Pass "
            "database=<TinyDB instance> as a class keyword when "
            f"defining the model, or call {model_name}.bind("
            "database=...) before using it.",
        )

__init__ ¤

__init__(model_name: str) -> None

Initialize with the name of the unbound model class.

Source code in src/tinydantic/_errors.py
def __init__(self, model_name: str) -> None:
    """Initialize with the name of the unbound model class."""
    super().__init__(
        f"No database is bound to {model_name!r}. Pass "
        "database=<TinyDB instance> as a class keyword when "
        f"defining the model, or call {model_name}.bind("
        "database=...) before using it.",
    )

DocumentIDRequiredError ¤

Bases: TinydanticError

Required document ID is missing.

Raised by instance operations that address a stored document by its id (replace(), delete()) when the instance was never inserted, so its id is still None.

Source code in src/tinydantic/_errors.py
class DocumentIDRequiredError(TinydanticError):
    """Required document ID is missing.

    Raised by instance operations that address a stored document by
    its id (``replace()``, ``delete()``) when the instance was never
    inserted, so its ``id`` is still ``None``.
    """

    def __init__(self, *, model_name: str, operation: str) -> None:
        """Initialize with the model name and attempted operation."""
        super().__init__(
            f"Cannot {operation}() a {model_name!r} instance whose id "
            "is None — insert() or save() it first",
        )

__init__ ¤

__init__(*, model_name: str, operation: str) -> None

Initialize with the model name and attempted operation.

Source code in src/tinydantic/_errors.py
def __init__(self, *, model_name: str, operation: str) -> None:
    """Initialize with the model name and attempted operation."""
    super().__init__(
        f"Cannot {operation}() a {model_name!r} instance whose id "
        "is None — insert() or save() it first",
    )

DocumentNotFoundError ¤

Bases: TinydanticError

Requested document is not found.

The message names the model, the table, and — when the lookup was by id — the missing document id, so the error is actionable without a debugger.

Source code in src/tinydantic/_errors.py
class DocumentNotFoundError(TinydanticError):
    """Requested document is not found.

    The message names the model, the table, and — when the lookup was
    by id — the missing document id, so the error is actionable
    without a debugger.
    """

    def __init__(
        self,
        *,
        model_name: str,
        table_name: str,
        doc_id: int | None = None,
    ) -> None:
        """Initialize with the model, table, and optional id context."""
        if doc_id is not None:
            selector = f"with id {doc_id}"
        else:
            selector = "matching the given query"
        super().__init__(
            f"No document {selector} in table {table_name!r} "
            f"(model {model_name!r})",
        )

__init__ ¤

__init__(
    *,
    model_name: str,
    table_name: str,
    doc_id: int | None = None
) -> None

Initialize with the model, table, and optional id context.

Source code in src/tinydantic/_errors.py
def __init__(
    self,
    *,
    model_name: str,
    table_name: str,
    doc_id: int | None = None,
) -> None:
    """Initialize with the model, table, and optional id context."""
    if doc_id is not None:
        selector = f"with id {doc_id}"
    else:
        selector = "matching the given query"
    super().__init__(
        f"No document {selector} in table {table_name!r} "
        f"(model {model_name!r})",
    )

TinydanticConfig ¤

Bases: TypedDict

Configuration options for tinydantic models.

This is a plain TypedDict — deliberately NOT a pydantic.ConfigDict subclass; see the module docstring for the design rationale. Values are provided as class keyword arguments:

class User(TinydanticModel, database=db, table_name="users"):
    name: str
Source code in src/tinydantic/_config.py
class TinydanticConfig(TypedDict, total=False):
    """Configuration options for tinydantic models.

    This is a plain [TypedDict][typing.TypedDict] — deliberately NOT a
    ``pydantic.ConfigDict`` subclass; see the module docstring for the
    design rationale. Values are provided as class keyword arguments:

    ```python
    class User(TinydanticModel, database=db, table_name="users"):
        name: str
    ```
    """

    database: TinyDB
    """TinyDB database where documents of this model are stored."""

    table_name: str | None
    """Database table name for documents of this model.

    When unset (or falsy), the table name is derived from the model
    class name converted to snake_case — for example, a model class
    named ``AdminUser`` uses the table ``admin_user``.
    """

database instance-attribute ¤

database: TinyDB

TinyDB database where documents of this model are stored.

table_name instance-attribute ¤

table_name: str | None

Database table name for documents of this model.

When unset (or falsy), the table name is derived from the model class name converted to snake_case — for example, a model class named AdminUser uses the table admin_user.

TinydanticError ¤

Bases: Exception

Base class for tinydantic errors.

Source code in src/tinydantic/_errors.py
class TinydanticError(Exception):
    """Base class for `tinydantic` errors."""

TinydanticModel ¤

Bases: BaseModel

Base class for tinydantic models.

Subclass to define a document model, passing tinydantic configuration as class keyword arguments:

from tinydb import TinyDB
from tinydantic import TinydanticModel

db = TinyDB("db.json")


class User(TinydanticModel, database=db, table_name="users"):
    name: str

Configuration is stored per class in __tinydantic_config__ and resolved by walking the MRO — deliberately NOT in pydantic's model_config; see the tinydantic._config module docstring for the design rationale (pydantic#9992).

Source code in src/tinydantic/_model.py
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
class TinydanticModel(BaseModel, metaclass=TinydanticModelMetaclass):
    """Base class for tinydantic models.

    Subclass to define a document model, passing tinydantic
    configuration as class keyword arguments:

    ```python
    from tinydb import TinyDB
    from tinydantic import TinydanticModel

    db = TinyDB("db.json")


    class User(TinydanticModel, database=db, table_name="users"):
        name: str
    ```

    Configuration is stored per class in ``__tinydantic_config__`` and
    resolved by walking the MRO — deliberately NOT in pydantic's
    ``model_config``; see the ``tinydantic._config`` module docstring
    for the design rationale (pydantic#9992).
    """

    model_config: ClassVar[ConfigDict] = ConfigDict(
        # Reserve the tinydantic_ prefix so future tinydantic methods
        # cannot collide with user-defined fields (the use case Samuel
        # Colvin described in pydantic#10315).
        protected_namespaces=("tinydantic_",),
    )

    __tinydantic_config__: ClassVar[TinydanticConfig] = {}

    # --- model fields ---

    id: int | None = Field(
        default=None,
        description="Document ID",
    )

    # --- configuration ---

    def __init_subclass__(
        cls,
        database: TinyDB | None = None,
        table_name: str | None = None,
        **kwargs: Any,
    ) -> None:
        """Capture tinydantic class keywords.

        Pydantic pops its own known config keys from class keyword
        arguments and forwards the rest here (a public extension
        point), so no metaclass involvement is needed for
        configuration. Only explicitly provided values are stored on
        this class — ``None`` means "not provided", and resolution
        falls through to base classes via
        ``get_config_value`` in ``tinydantic._config``.
        """
        super().__init_subclass__(**kwargs)
        config: TinydanticConfig = {}
        if database is not None:
            config["database"] = database
        if table_name is not None:
            config["table_name"] = table_name
        setattr(cls, CONFIG_ATTR, config)

    @classmethod
    def __pydantic_init_subclass__(cls, **kwargs: Any) -> None:
        """Validate config after pydantic finishes building the class.

        Raises:
            AmbiguousConfigError: If unrelated base classes supply
                conflicting tinydantic config (see
                ``check_config_ambiguity`` in ``tinydantic._config``).
        """
        super().__pydantic_init_subclass__(**kwargs)
        check_config_ambiguity(cls)

    @classmethod
    def bind(
        cls,
        *,
        database: TinyDB | None = None,
        table_name: str | None = None,
    ) -> None:
        """Bind or rebind tinydantic config after class definition.

        The late-binding escape hatch for tests and application
        factories where no TinyDB instance exists at import time:

        ```python
        class User(TinydanticModel):
            name: str


        User.bind(database=TinyDB("db.json"))
        ```

        Only the keys passed are updated; other keys keep their
        current (possibly inherited) values. Binding a subclass never
        affects its parents.
        """
        config = cast(
            "TinydanticConfig",
            dict(cls.__dict__.get(CONFIG_ATTR, {})),
        )
        if database is not None:
            config["database"] = database
        if table_name is not None:
            config["table_name"] = table_name
        setattr(cls, CONFIG_ATTR, config)

    @classmethod
    def get_database(cls) -> TinyDB:
        """Get the TinyDB database this model is bound to.

        Returns:
            The bound TinyDB database.

        Raises:
            DatabaseNotBoundError: If no database is configured
                anywhere in the class hierarchy.
        """
        database: TinyDB | None = get_config_value(cls, "database")
        if database is None:
            raise DatabaseNotBoundError(cls.__name__)
        return database

    @classmethod
    def get_table(cls) -> Table:
        """Get the TinyDB table for this model.

        Uses the configured ``table_name`` when set, otherwise the
        snake_case form of the class name (``AdminUser`` →
        ``admin_user``).
        """
        table_name: str | None = get_config_value(cls, "table_name")
        if not table_name:
            return cls.get_database().table(name=to_snake(cls.__name__))
        return cls.get_database().table(name=table_name)

    @classmethod
    def from_tinydb_document(cls, document: Mapping) -> Self:
        """Validate a TinyDB document into a model instance.

        Runs ``document`` through pydantic validation, then maps
        TinyDB's ``doc_id`` onto the model's ``id`` field: when
        ``document`` is a [Document][tinydb.table.Document] (as
        returned by TinyDB reads), its
        [doc_id][tinydb.table.Document.doc_id] becomes the instance
        ``id``. A plain mapping carries no ``doc_id``, so ``id`` keeps
        its default of ``None``. This is the inverse of
        [to_tinydb_document][tinydantic.TinydanticModel.to_tinydb_document],
        which maps ``id`` back to ``doc_id`` and never stores it in the
        document body.

        Args:
            document: A TinyDB document (or plain mapping) to validate.

        Returns:
            A validated model instance, with ``id`` set from ``doc_id``
            when ``document`` carries one.
        """
        instance = cls.model_validate(document)
        if isinstance(document, Document):
            instance.id = document.doc_id
        return instance

    @classmethod
    def insert_multiple(cls, documents: Iterable[Self]) -> list[Self]:
        """Insert several models at once.

        Serializes each model with
        [to_tinydb_document][tinydantic.TinydanticModel.to_tinydb_document]
        and hands them to [tinydb.table.Table.insert_multiple][].
        Exactly like [insert][tinydantic.TinydanticModel.insert], each
        model's ``id`` is set in place to the document id TinyDB
        assigned, and the same instances are returned in insertion
        order.

        Args:
            documents: The models to insert.

        Returns:
            The inserted models, with ``id`` set, in insertion order.
        """
        docs = list(documents)
        doc_ids = cls.get_table().insert_multiple(
            [doc.to_tinydb_document() for doc in docs],
        )
        for doc, doc_id in zip(docs, doc_ids, strict=True):
            doc.id = doc_id
        return docs

    @classmethod
    def all(cls) -> list[Self]:
        """Get every document in the table as validated models.

        Iterates the whole table and validates each document via
        [from_tinydb_document][tinydantic.TinydanticModel.from_tinydb_document],
        so every returned instance has its ``id`` populated from the
        stored ``doc_id``.

        Returns:
            All documents in the table as validated models.
        """
        return [cls.from_tinydb_document(doc) for doc in iter(cls.get_table())]

    @classmethod
    def search(cls, cond: QueryLike) -> list[Self]:
        """Get all documents matching ``cond`` as validated models."""
        return [
            cls.from_tinydb_document(doc)
            for doc in cls.get_table().search(cond)
        ]

    @overload
    @classmethod
    def get(cls, cond: QueryLike) -> Self | None: ...

    @overload
    @classmethod
    def get(cls, *, doc_id: int) -> Self | None: ...

    @overload
    @classmethod
    def get(cls, *, doc_ids: list[int]) -> list[Self]: ...

    @classmethod
    def get(
        cls,
        cond: QueryLike | None = None,
        *,
        doc_id: int | None = None,
        doc_ids: list[int] | None = None,
    ) -> Self | list[Self] | None:
        """Get one document (or several by id) as validated models.

        Mirrors [tinydb.table.Table.get][], with one tightening: at
        most one of ``cond``, ``doc_id``, ``doc_ids`` may be provided
        (TinyDB silently applies a precedence order; tinydantic raises
        ``ValueError``). The typed variants
        [get_by_cond][tinydantic.TinydanticModel.get_by_cond],
        [get_by_id][tinydantic.TinydanticModel.get_by_id], and
        [get_by_ids][tinydantic.TinydanticModel.get_by_ids] offer
        precise return types per call shape.

        When ``doc_ids`` is given, TinyDB returns only the documents
        that exist (missing ids are silently skipped), so the result is
        a ``list`` that may be shorter than the ids requested and is
        ordered by storage iteration, not by the ids passed in.

        Raises:
            ValueError: If more than one selector is provided.
        """
        provided = [s for s in (cond, doc_id, doc_ids) if s is not None]
        if len(provided) > 1:
            msg = "Provide at most one of cond, doc_id, or doc_ids"
            raise ValueError(msg)

        result = cls.get_table().get(
            cond=cond,
            doc_id=doc_id,
            doc_ids=doc_ids,
        )

        if result is None:
            return None

        if isinstance(result, Document):
            return cls.from_tinydb_document(result)

        if isinstance(result, list):
            return [cls.from_tinydb_document(doc) for doc in result]

        raise TypeError

    @classmethod
    def get_by_cond(cls, cond: QueryLike) -> Self | None:
        """Get the first document matching ``cond``, or ``None``."""
        return cls.get(cond)

    @classmethod
    def get_by_id(cls, doc_id: int) -> Self | None:
        """Get the document with the given id, or ``None``."""
        return cls.get(doc_id=doc_id)

    @classmethod
    def get_by_ids(cls, doc_ids: list[int]) -> list[Self]:
        """Get documents for the given ids (see get() for semantics)."""
        return cls.get(doc_ids=doc_ids)

    @overload
    @classmethod
    def get_or_raise(cls, cond: QueryLike) -> Self: ...

    @overload
    @classmethod
    def get_or_raise(cls, *, doc_id: int) -> Self: ...

    @classmethod
    def get_or_raise(
        cls,
        cond: QueryLike | None = None,
        *,
        doc_id: int | None = None,
    ) -> Self:
        """Get one document, raising instead of returning ``None``.

        The strict counterpart to
        [get][tinydantic.TinydanticModel.get] for call sites where a
        missing document is an error rather than an expected outcome
        (request handlers, lookups by known id, ...). Accepts exactly
        one selector: a query condition or a ``doc_id``. There is no
        ``doc_ids`` form — TinyDB silently skips missing ids in bulk
        gets, so "raise if missing" has no single obvious meaning
        there.

        Args:
            cond: The query condition to match.
            doc_id: The document id to fetch.

        Returns:
            The validated model instance.

        Raises:
            DocumentNotFoundError: If no matching document exists.
            ValueError: If no selector or both selectors are provided.
        """
        if cond is not None and doc_id is None:
            result = cls.get(cond)
        elif doc_id is not None and cond is None:
            result = cls.get(doc_id=doc_id)
        else:
            msg = "Provide exactly one of cond or doc_id"
            raise ValueError(msg)
        if result is None:
            raise DocumentNotFoundError(
                model_name=cls.__name__,
                table_name=cls.get_table().name,
                doc_id=doc_id,
            )
        return result

    @classmethod
    def contains(
        cls,
        cond: QueryLike | None = None,
        *,
        doc_id: int | None = None,
    ) -> bool:
        """Check whether a matching document exists.

        Raises:
            ValueError: If both ``cond`` and ``doc_id`` are provided.
        """
        if cond is not None and doc_id is not None:
            msg = "Provide at most one of cond or doc_id"
            raise ValueError(msg)
        return cls.get_table().contains(cond=cond, doc_id=doc_id)

    @classmethod
    def _field_adapter(cls, field_name: str) -> TypeAdapter[Any]:
        """Get (or build and cache) a TypeAdapter for a model field.

        The adapter is built from the field's full annotation
        (including ``Field(...)`` metadata, via
        [rebuild_annotation][pydantic.fields.FieldInfo.rebuild_annotation])
        and cached on this class, so repeated ``update()`` calls pay
        the construction cost once per field.
        """
        adapters: dict[str, TypeAdapter[Any]] | None = cls.__dict__.get(
            _FIELD_ADAPTERS_ATTR,
        )
        if adapters is None:
            adapters = {}
            setattr(cls, _FIELD_ADAPTERS_ATTR, adapters)
        adapter = adapters.get(field_name)
        if adapter is None:
            field_info = cls.model_fields[field_name]
            adapter = TypeAdapter(field_info.rebuild_annotation())
            adapters[field_name] = adapter
        return adapter

    @classmethod
    def _serialize_update_fields(cls, fields: Mapping) -> dict[str, Any]:
        """Validate and JSON-serialize known field values in a mapping.

        Each key that names a model field has its value validated
        against that field's type and serialized in JSON mode — the
        same treatment ``insert()``/``save()`` give whole models — so
        rich values (datetime, UUID, nested models, ...) land in
        storage as JSON-safe primitives. Keys that are not model
        fields pass through unchanged.

        Raises:
            pydantic.ValidationError: If a value fails validation
                against its field's type.
        """
        serialized: dict[str, Any] = {}
        for key, value in fields.items():
            if key in cls.model_fields:
                adapter = cls._field_adapter(key)
                serialized[key] = adapter.dump_python(
                    adapter.validate_python(value),
                    mode="json",
                )
            else:
                serialized[key] = value
        return serialized

    @classmethod
    def update(
        cls,
        fields: Mapping | Callable[[Mapping], None],
        cond: QueryLike | None = None,
        *,
        doc_ids: Iterable[int] | None = None,
    ) -> list[int]:
        """Update matching documents with new fields or a transform.

        A ``fields`` mapping gets the same treatment ``insert()`` and
        ``save()`` give whole models: each value that belongs to a
        model field is validated against that field's type and
        serialized to a JSON-safe primitive before it reaches storage
        (keys that are not model fields pass through unchanged). A
        transform callable is handed to TinyDB as-is — what it writes
        is up to you.

        Returns:
            The ids of all updated documents.

        Raises:
            pydantic.ValidationError: If a mapping value fails
                validation against its field's type.
        """
        if not callable(fields):
            fields = cls._serialize_update_fields(fields)
        return cls.get_table().update(
            # See replace() for why this cast is needed.
            # TODO @cdwilson: remove this cast once the annotation is
            # fixed in TinyDB.
            cast("Callable[[Mapping], None]", fields),
            cond=cond,
            doc_ids=doc_ids,
        )

    @classmethod
    def update_multiple(
        cls,
        updates: Iterable[
            tuple[
                Mapping | Callable[[Mapping], None],
                QueryLike,
            ]
        ],
    ) -> list[int]:
        """Apply several (fields_or_transform, cond) updates at once.

        Each update's fields mapping is validated and serialized
        exactly as in [update][tinydantic.TinydanticModel.update];
        transform callables pass through to TinyDB as-is.

        Returns:
            The ids of all updated documents.

        Raises:
            pydantic.ValidationError: If a mapping value fails
                validation against its field's type.
        """
        prepared = [
            (
                fields
                if callable(fields)
                else cls._serialize_update_fields(fields),
                cond,
            )
            for fields, cond in updates
        ]
        return cls.get_table().update_multiple(
            # See replace() for why this cast is needed.
            cast(
                "Iterable[tuple[Callable[[Mapping], None], QueryLike]]",
                prepared,
            ),
        )

    @classmethod
    def upsert(
        cls,
        document: Self,
        cond: QueryLike | None = None,
    ) -> list[int]:
        """Update documents matching ``cond``, or insert ``document``.

        Returns:
            The ids of the updated (or inserted) documents.
        """
        return cls.get_table().upsert(
            document.to_tinydb_document(force_dict=cond is not None),
            cond,
        )

    @classmethod
    def remove(
        cls,
        cond: QueryLike | None = None,
        *,
        doc_ids: Iterable[int] | None = None,
    ) -> list[int]:
        """Remove matching documents.

        Returns:
            The ids of all removed documents.
        """
        return cls.get_table().remove(cond=cond, doc_ids=doc_ids)

    @classmethod
    def truncate(cls) -> None:
        """Remove every document from the table.

        Delegates to [tinydb.table.Table.truncate][], leaving the table
        empty and resetting its document id counter.
        """
        cls.get_table().truncate()

    @classmethod
    def count(cls, cond: QueryLike | None = None) -> int:
        """Count the documents matching ``cond``, or all documents.

        With a condition, delegates to [tinydb.table.Table.count][].
        Without one, returns the total number of documents in the
        table (``len(table)``) — TinyDB itself spells this
        ``len(db.table(...))``; tinydantic folds it into ``count()``
        so "how many documents are there?" needs no query object.

        Args:
            cond: The query condition to match. When omitted, every
                document in the table is counted.

        Returns:
            The number of matching (or total) documents.
        """
        if cond is None:
            return len(cls.get_table())
        return cls.get_table().count(cond)

    @classmethod
    def clear_cache(cls) -> None:
        """Clear the table's query cache.

        Delegates to [tinydb.table.Table.clear_cache][]. TinyDB caches
        query results per table; call this to discard those cached
        results (for example after mutating storage out of band).
        """
        cls.get_table().clear_cache()

    # --- instance methods ---

    def to_tinydb_document(
        self,
        *,
        force_dict: bool = False,
    ) -> dict[str, Any] | Document:
        """Convert this model to a TinyDB-storable document.

        Uses JSON-mode serialization so rich pydantic types (datetime,
        UUID, enums, nested models, ...) become JSON-safe primitives
        that round-trip through any TinyDB storage. The ``id`` field is
        never embedded in the document — it maps to TinyDB's
        ``doc_id``.

        Args:
            force_dict: Return a plain dict even when ``id`` is set
                (otherwise a [Document][tinydb.table.Document] carrying
                ``doc_id`` is returned).
        """
        doc = self.model_dump(mode="json", exclude={"id"})

        if (force_dict is False) and (self.id is not None):
            doc = Document(doc, self.id)

        return doc

    def insert(self) -> Self:
        """Insert this model as a new document.

        Serializes the model with
        [to_tinydb_document][tinydantic.TinydanticModel.to_tinydb_document]
        and inserts it via [tinydb.table.Table.insert][]. When ``id`` is
        unset it is assigned the id TinyDB generates; when ``id`` is
        already set that value is used as the document id.

        Returns:
            This instance, with ``id`` set to the new document id.

        Raises:
            ValueError: If ``id`` is set to an id that already exists in
                the table (raised by TinyDB).
        """
        self.id = self.get_table().insert(self.to_tinydb_document())

        return self

    def replace(self) -> None:
        """Overwrite this model's stored document in place.

        Requires ``id`` to be set. Unlike
        [update][tinydantic.TinydanticModel.update], which merges
        fields, ``replace`` swaps the entire stored document for this
        model's current serialized state, so fields absent from the
        model are removed. Unlike
        [save][tinydantic.TinydanticModel.save], which re-inserts a
        missing document, ``replace`` requires the document to already
        exist.

        Raises:
            DocumentIDRequiredError: If ``id`` is not set (the model
                was never inserted).
            DocumentNotFoundError: If no document with this ``id``
                exists in the table.
        """
        if self.id is None:
            raise DocumentIDRequiredError(
                model_name=type(self).__name__,
                operation="replace",
            )

        try:
            updated_doc_ids = self.get_table().update(
                # In TinyDB, the Table.update/update_multiple methods
                # currently annotate the fields parameter with the type
                # Callable[[Mapping], None].
                #
                # However, the doc parameter that is passed to this
                # transform function is actually a python dict (which
                # is a type of MutableMapping).
                #
                # This cast is simply a band-aid to keep the type
                # checker happy.
                #
                # TODO @cdwilson: remove this cast once the annotation
                # is fixed in TinyDB.
                cast(
                    "Callable[[Mapping], None]",
                    replace(self.to_tinydb_document(force_dict=True)),
                ),
                doc_ids=[self.id],
            )
        except KeyError:
            raise DocumentNotFoundError(
                model_name=type(self).__name__,
                table_name=self.get_table().name,
                doc_id=self.id,
            ) from None

        if not updated_doc_ids:
            raise DocumentNotFoundError(
                model_name=type(self).__name__,
                table_name=self.get_table().name,
                doc_id=self.id,
            )

    def delete(self) -> None:
        """Remove this model's document from its table.

        Raises:
            DocumentIDRequiredError: If ``id`` is not set (the model
                was never inserted).
            DocumentNotFoundError: If no document with this ``id``
                exists in the table.
        """
        if self.id is None:
            raise DocumentIDRequiredError(
                model_name=type(self).__name__,
                operation="delete",
            )
        try:
            removed = self.get_table().remove(doc_ids=[self.id])
        except KeyError:
            raise DocumentNotFoundError(
                model_name=type(self).__name__,
                table_name=self.get_table().name,
                doc_id=self.id,
            ) from None
        if not removed:
            raise DocumentNotFoundError(
                model_name=type(self).__name__,
                table_name=self.get_table().name,
                doc_id=self.id,
            )

    def save(self) -> Self:
        """Insert this model if it is new, otherwise update it by id.

        If ``id`` is set but the document no longer exists in the
        table, it is re-inserted under the same id (TinyDB upsert
        semantics) — unlike ``replace()``/``delete()``, which raise
        [DocumentNotFoundError][tinydantic.DocumentNotFoundError].

        Returns:
            This instance (with ``id`` set if it was newly inserted).
        """
        if self.id is None:
            return self.insert()
        self.id = self.get_table().upsert(self.to_tinydb_document())[0]
        return self

__init_subclass__ ¤

__init_subclass__(
    database: TinyDB | None = None,
    table_name: str | None = None,
    **kwargs: Any
) -> None

Capture tinydantic class keywords.

Pydantic pops its own known config keys from class keyword arguments and forwards the rest here (a public extension point), so no metaclass involvement is needed for configuration. Only explicitly provided values are stored on this class — None means "not provided", and resolution falls through to base classes via get_config_value in tinydantic._config.

Source code in src/tinydantic/_model.py
def __init_subclass__(
    cls,
    database: TinyDB | None = None,
    table_name: str | None = None,
    **kwargs: Any,
) -> None:
    """Capture tinydantic class keywords.

    Pydantic pops its own known config keys from class keyword
    arguments and forwards the rest here (a public extension
    point), so no metaclass involvement is needed for
    configuration. Only explicitly provided values are stored on
    this class — ``None`` means "not provided", and resolution
    falls through to base classes via
    ``get_config_value`` in ``tinydantic._config``.
    """
    super().__init_subclass__(**kwargs)
    config: TinydanticConfig = {}
    if database is not None:
        config["database"] = database
    if table_name is not None:
        config["table_name"] = table_name
    setattr(cls, CONFIG_ATTR, config)

__pydantic_init_subclass__ classmethod ¤

__pydantic_init_subclass__(**kwargs: Any) -> None

Validate config after pydantic finishes building the class.

Raises:

Type Description
AmbiguousConfigError

If unrelated base classes supply conflicting tinydantic config (see check_config_ambiguity in tinydantic._config).

Source code in src/tinydantic/_model.py
@classmethod
def __pydantic_init_subclass__(cls, **kwargs: Any) -> None:
    """Validate config after pydantic finishes building the class.

    Raises:
        AmbiguousConfigError: If unrelated base classes supply
            conflicting tinydantic config (see
            ``check_config_ambiguity`` in ``tinydantic._config``).
    """
    super().__pydantic_init_subclass__(**kwargs)
    check_config_ambiguity(cls)

all classmethod ¤

all() -> list[Self]

Get every document in the table as validated models.

Iterates the whole table and validates each document via from_tinydb_document, so every returned instance has its id populated from the stored doc_id.

Returns:

Type Description
list[Self]

All documents in the table as validated models.

Source code in src/tinydantic/_model.py
@classmethod
def all(cls) -> list[Self]:
    """Get every document in the table as validated models.

    Iterates the whole table and validates each document via
    [from_tinydb_document][tinydantic.TinydanticModel.from_tinydb_document],
    so every returned instance has its ``id`` populated from the
    stored ``doc_id``.

    Returns:
        All documents in the table as validated models.
    """
    return [cls.from_tinydb_document(doc) for doc in iter(cls.get_table())]

bind classmethod ¤

bind(
    *,
    database: TinyDB | None = None,
    table_name: str | None = None
) -> None

Bind or rebind tinydantic config after class definition.

The late-binding escape hatch for tests and application factories where no TinyDB instance exists at import time:

class User(TinydanticModel):
    name: str


User.bind(database=TinyDB("db.json"))

Only the keys passed are updated; other keys keep their current (possibly inherited) values. Binding a subclass never affects its parents.

Source code in src/tinydantic/_model.py
@classmethod
def bind(
    cls,
    *,
    database: TinyDB | None = None,
    table_name: str | None = None,
) -> None:
    """Bind or rebind tinydantic config after class definition.

    The late-binding escape hatch for tests and application
    factories where no TinyDB instance exists at import time:

    ```python
    class User(TinydanticModel):
        name: str


    User.bind(database=TinyDB("db.json"))
    ```

    Only the keys passed are updated; other keys keep their
    current (possibly inherited) values. Binding a subclass never
    affects its parents.
    """
    config = cast(
        "TinydanticConfig",
        dict(cls.__dict__.get(CONFIG_ATTR, {})),
    )
    if database is not None:
        config["database"] = database
    if table_name is not None:
        config["table_name"] = table_name
    setattr(cls, CONFIG_ATTR, config)

clear_cache classmethod ¤

clear_cache() -> None

Clear the table's query cache.

Delegates to tinydb.table.Table.clear_cache. TinyDB caches query results per table; call this to discard those cached results (for example after mutating storage out of band).

Source code in src/tinydantic/_model.py
@classmethod
def clear_cache(cls) -> None:
    """Clear the table's query cache.

    Delegates to [tinydb.table.Table.clear_cache][]. TinyDB caches
    query results per table; call this to discard those cached
    results (for example after mutating storage out of band).
    """
    cls.get_table().clear_cache()

contains classmethod ¤

contains(
    cond: QueryLike | None = None,
    *,
    doc_id: int | None = None
) -> bool

Check whether a matching document exists.

Raises:

Type Description
ValueError

If both cond and doc_id are provided.

Source code in src/tinydantic/_model.py
@classmethod
def contains(
    cls,
    cond: QueryLike | None = None,
    *,
    doc_id: int | None = None,
) -> bool:
    """Check whether a matching document exists.

    Raises:
        ValueError: If both ``cond`` and ``doc_id`` are provided.
    """
    if cond is not None and doc_id is not None:
        msg = "Provide at most one of cond or doc_id"
        raise ValueError(msg)
    return cls.get_table().contains(cond=cond, doc_id=doc_id)

count classmethod ¤

count(cond: QueryLike | None = None) -> int

Count the documents matching cond, or all documents.

With a condition, delegates to tinydb.table.Table.count. Without one, returns the total number of documents in the table (len(table)) — TinyDB itself spells this len(db.table(...)); tinydantic folds it into count() so "how many documents are there?" needs no query object.

Parameters:

Name Type Description Default
cond QueryLike | None

The query condition to match. When omitted, every document in the table is counted.

None

Returns:

Type Description
int

The number of matching (or total) documents.

Source code in src/tinydantic/_model.py
@classmethod
def count(cls, cond: QueryLike | None = None) -> int:
    """Count the documents matching ``cond``, or all documents.

    With a condition, delegates to [tinydb.table.Table.count][].
    Without one, returns the total number of documents in the
    table (``len(table)``) — TinyDB itself spells this
    ``len(db.table(...))``; tinydantic folds it into ``count()``
    so "how many documents are there?" needs no query object.

    Args:
        cond: The query condition to match. When omitted, every
            document in the table is counted.

    Returns:
        The number of matching (or total) documents.
    """
    if cond is None:
        return len(cls.get_table())
    return cls.get_table().count(cond)

delete ¤

delete() -> None

Remove this model's document from its table.

Raises:

Type Description
DocumentIDRequiredError

If id is not set (the model was never inserted).

DocumentNotFoundError

If no document with this id exists in the table.

Source code in src/tinydantic/_model.py
def delete(self) -> None:
    """Remove this model's document from its table.

    Raises:
        DocumentIDRequiredError: If ``id`` is not set (the model
            was never inserted).
        DocumentNotFoundError: If no document with this ``id``
            exists in the table.
    """
    if self.id is None:
        raise DocumentIDRequiredError(
            model_name=type(self).__name__,
            operation="delete",
        )
    try:
        removed = self.get_table().remove(doc_ids=[self.id])
    except KeyError:
        raise DocumentNotFoundError(
            model_name=type(self).__name__,
            table_name=self.get_table().name,
            doc_id=self.id,
        ) from None
    if not removed:
        raise DocumentNotFoundError(
            model_name=type(self).__name__,
            table_name=self.get_table().name,
            doc_id=self.id,
        )

from_tinydb_document classmethod ¤

from_tinydb_document(document: Mapping) -> Self

Validate a TinyDB document into a model instance.

Runs document through pydantic validation, then maps TinyDB's doc_id onto the model's id field: when document is a Document (as returned by TinyDB reads), its doc_id becomes the instance id. A plain mapping carries no doc_id, so id keeps its default of None. This is the inverse of to_tinydb_document, which maps id back to doc_id and never stores it in the document body.

Parameters:

Name Type Description Default
document Mapping

A TinyDB document (or plain mapping) to validate.

required

Returns:

Type Description
Self

A validated model instance, with id set from doc_id

Self

when document carries one.

Source code in src/tinydantic/_model.py
@classmethod
def from_tinydb_document(cls, document: Mapping) -> Self:
    """Validate a TinyDB document into a model instance.

    Runs ``document`` through pydantic validation, then maps
    TinyDB's ``doc_id`` onto the model's ``id`` field: when
    ``document`` is a [Document][tinydb.table.Document] (as
    returned by TinyDB reads), its
    [doc_id][tinydb.table.Document.doc_id] becomes the instance
    ``id``. A plain mapping carries no ``doc_id``, so ``id`` keeps
    its default of ``None``. This is the inverse of
    [to_tinydb_document][tinydantic.TinydanticModel.to_tinydb_document],
    which maps ``id`` back to ``doc_id`` and never stores it in the
    document body.

    Args:
        document: A TinyDB document (or plain mapping) to validate.

    Returns:
        A validated model instance, with ``id`` set from ``doc_id``
        when ``document`` carries one.
    """
    instance = cls.model_validate(document)
    if isinstance(document, Document):
        instance.id = document.doc_id
    return instance

get classmethod ¤

get(cond: QueryLike) -> Self | None
get(*, doc_id: int) -> Self | None
get(*, doc_ids: list[int]) -> list[Self]
get(
    cond: QueryLike | None = None,
    *,
    doc_id: int | None = None,
    doc_ids: list[int] | None = None
) -> Self | list[Self] | None

Get one document (or several by id) as validated models.

Mirrors tinydb.table.Table.get, with one tightening: at most one of cond, doc_id, doc_ids may be provided (TinyDB silently applies a precedence order; tinydantic raises ValueError). The typed variants get_by_cond, get_by_id, and get_by_ids offer precise return types per call shape.

When doc_ids is given, TinyDB returns only the documents that exist (missing ids are silently skipped), so the result is a list that may be shorter than the ids requested and is ordered by storage iteration, not by the ids passed in.

Raises:

Type Description
ValueError

If more than one selector is provided.

Source code in src/tinydantic/_model.py
@classmethod
def get(
    cls,
    cond: QueryLike | None = None,
    *,
    doc_id: int | None = None,
    doc_ids: list[int] | None = None,
) -> Self | list[Self] | None:
    """Get one document (or several by id) as validated models.

    Mirrors [tinydb.table.Table.get][], with one tightening: at
    most one of ``cond``, ``doc_id``, ``doc_ids`` may be provided
    (TinyDB silently applies a precedence order; tinydantic raises
    ``ValueError``). The typed variants
    [get_by_cond][tinydantic.TinydanticModel.get_by_cond],
    [get_by_id][tinydantic.TinydanticModel.get_by_id], and
    [get_by_ids][tinydantic.TinydanticModel.get_by_ids] offer
    precise return types per call shape.

    When ``doc_ids`` is given, TinyDB returns only the documents
    that exist (missing ids are silently skipped), so the result is
    a ``list`` that may be shorter than the ids requested and is
    ordered by storage iteration, not by the ids passed in.

    Raises:
        ValueError: If more than one selector is provided.
    """
    provided = [s for s in (cond, doc_id, doc_ids) if s is not None]
    if len(provided) > 1:
        msg = "Provide at most one of cond, doc_id, or doc_ids"
        raise ValueError(msg)

    result = cls.get_table().get(
        cond=cond,
        doc_id=doc_id,
        doc_ids=doc_ids,
    )

    if result is None:
        return None

    if isinstance(result, Document):
        return cls.from_tinydb_document(result)

    if isinstance(result, list):
        return [cls.from_tinydb_document(doc) for doc in result]

    raise TypeError

get_by_cond classmethod ¤

get_by_cond(cond: QueryLike) -> Self | None

Get the first document matching cond, or None.

Source code in src/tinydantic/_model.py
@classmethod
def get_by_cond(cls, cond: QueryLike) -> Self | None:
    """Get the first document matching ``cond``, or ``None``."""
    return cls.get(cond)

get_by_id classmethod ¤

get_by_id(doc_id: int) -> Self | None

Get the document with the given id, or None.

Source code in src/tinydantic/_model.py
@classmethod
def get_by_id(cls, doc_id: int) -> Self | None:
    """Get the document with the given id, or ``None``."""
    return cls.get(doc_id=doc_id)

get_by_ids classmethod ¤

get_by_ids(doc_ids: list[int]) -> list[Self]

Get documents for the given ids (see get() for semantics).

Source code in src/tinydantic/_model.py
@classmethod
def get_by_ids(cls, doc_ids: list[int]) -> list[Self]:
    """Get documents for the given ids (see get() for semantics)."""
    return cls.get(doc_ids=doc_ids)

get_database classmethod ¤

get_database() -> TinyDB

Get the TinyDB database this model is bound to.

Returns:

Type Description
TinyDB

The bound TinyDB database.

Raises:

Type Description
DatabaseNotBoundError

If no database is configured anywhere in the class hierarchy.

Source code in src/tinydantic/_model.py
@classmethod
def get_database(cls) -> TinyDB:
    """Get the TinyDB database this model is bound to.

    Returns:
        The bound TinyDB database.

    Raises:
        DatabaseNotBoundError: If no database is configured
            anywhere in the class hierarchy.
    """
    database: TinyDB | None = get_config_value(cls, "database")
    if database is None:
        raise DatabaseNotBoundError(cls.__name__)
    return database

get_or_raise classmethod ¤

get_or_raise(cond: QueryLike) -> Self
get_or_raise(*, doc_id: int) -> Self
get_or_raise(
    cond: QueryLike | None = None,
    *,
    doc_id: int | None = None
) -> Self

Get one document, raising instead of returning None.

The strict counterpart to get for call sites where a missing document is an error rather than an expected outcome (request handlers, lookups by known id, ...). Accepts exactly one selector: a query condition or a doc_id. There is no doc_ids form — TinyDB silently skips missing ids in bulk gets, so "raise if missing" has no single obvious meaning there.

Parameters:

Name Type Description Default
cond QueryLike | None

The query condition to match.

None
doc_id int | None

The document id to fetch.

None

Returns:

Type Description
Self

The validated model instance.

Raises:

Type Description
DocumentNotFoundError

If no matching document exists.

ValueError

If no selector or both selectors are provided.

Source code in src/tinydantic/_model.py
@classmethod
def get_or_raise(
    cls,
    cond: QueryLike | None = None,
    *,
    doc_id: int | None = None,
) -> Self:
    """Get one document, raising instead of returning ``None``.

    The strict counterpart to
    [get][tinydantic.TinydanticModel.get] for call sites where a
    missing document is an error rather than an expected outcome
    (request handlers, lookups by known id, ...). Accepts exactly
    one selector: a query condition or a ``doc_id``. There is no
    ``doc_ids`` form — TinyDB silently skips missing ids in bulk
    gets, so "raise if missing" has no single obvious meaning
    there.

    Args:
        cond: The query condition to match.
        doc_id: The document id to fetch.

    Returns:
        The validated model instance.

    Raises:
        DocumentNotFoundError: If no matching document exists.
        ValueError: If no selector or both selectors are provided.
    """
    if cond is not None and doc_id is None:
        result = cls.get(cond)
    elif doc_id is not None and cond is None:
        result = cls.get(doc_id=doc_id)
    else:
        msg = "Provide exactly one of cond or doc_id"
        raise ValueError(msg)
    if result is None:
        raise DocumentNotFoundError(
            model_name=cls.__name__,
            table_name=cls.get_table().name,
            doc_id=doc_id,
        )
    return result

get_table classmethod ¤

get_table() -> Table

Get the TinyDB table for this model.

Uses the configured table_name when set, otherwise the snake_case form of the class name (AdminUseradmin_user).

Source code in src/tinydantic/_model.py
@classmethod
def get_table(cls) -> Table:
    """Get the TinyDB table for this model.

    Uses the configured ``table_name`` when set, otherwise the
    snake_case form of the class name (``AdminUser`` →
    ``admin_user``).
    """
    table_name: str | None = get_config_value(cls, "table_name")
    if not table_name:
        return cls.get_database().table(name=to_snake(cls.__name__))
    return cls.get_database().table(name=table_name)

insert ¤

insert() -> Self

Insert this model as a new document.

Serializes the model with to_tinydb_document and inserts it via tinydb.table.Table.insert. When id is unset it is assigned the id TinyDB generates; when id is already set that value is used as the document id.

Returns:

Type Description
Self

This instance, with id set to the new document id.

Raises:

Type Description
ValueError

If id is set to an id that already exists in the table (raised by TinyDB).

Source code in src/tinydantic/_model.py
def insert(self) -> Self:
    """Insert this model as a new document.

    Serializes the model with
    [to_tinydb_document][tinydantic.TinydanticModel.to_tinydb_document]
    and inserts it via [tinydb.table.Table.insert][]. When ``id`` is
    unset it is assigned the id TinyDB generates; when ``id`` is
    already set that value is used as the document id.

    Returns:
        This instance, with ``id`` set to the new document id.

    Raises:
        ValueError: If ``id`` is set to an id that already exists in
            the table (raised by TinyDB).
    """
    self.id = self.get_table().insert(self.to_tinydb_document())

    return self

insert_multiple classmethod ¤

insert_multiple(documents: Iterable[Self]) -> list[Self]

Insert several models at once.

Serializes each model with to_tinydb_document and hands them to tinydb.table.Table.insert_multiple. Exactly like insert, each model's id is set in place to the document id TinyDB assigned, and the same instances are returned in insertion order.

Parameters:

Name Type Description Default
documents Iterable[Self]

The models to insert.

required

Returns:

Type Description
list[Self]

The inserted models, with id set, in insertion order.

Source code in src/tinydantic/_model.py
@classmethod
def insert_multiple(cls, documents: Iterable[Self]) -> list[Self]:
    """Insert several models at once.

    Serializes each model with
    [to_tinydb_document][tinydantic.TinydanticModel.to_tinydb_document]
    and hands them to [tinydb.table.Table.insert_multiple][].
    Exactly like [insert][tinydantic.TinydanticModel.insert], each
    model's ``id`` is set in place to the document id TinyDB
    assigned, and the same instances are returned in insertion
    order.

    Args:
        documents: The models to insert.

    Returns:
        The inserted models, with ``id`` set, in insertion order.
    """
    docs = list(documents)
    doc_ids = cls.get_table().insert_multiple(
        [doc.to_tinydb_document() for doc in docs],
    )
    for doc, doc_id in zip(docs, doc_ids, strict=True):
        doc.id = doc_id
    return docs

remove classmethod ¤

remove(
    cond: QueryLike | None = None,
    *,
    doc_ids: Iterable[int] | None = None
) -> list[int]

Remove matching documents.

Returns:

Type Description
list[int]

The ids of all removed documents.

Source code in src/tinydantic/_model.py
@classmethod
def remove(
    cls,
    cond: QueryLike | None = None,
    *,
    doc_ids: Iterable[int] | None = None,
) -> list[int]:
    """Remove matching documents.

    Returns:
        The ids of all removed documents.
    """
    return cls.get_table().remove(cond=cond, doc_ids=doc_ids)

replace ¤

replace() -> None

Overwrite this model's stored document in place.

Requires id to be set. Unlike update, which merges fields, replace swaps the entire stored document for this model's current serialized state, so fields absent from the model are removed. Unlike save, which re-inserts a missing document, replace requires the document to already exist.

Raises:

Type Description
DocumentIDRequiredError

If id is not set (the model was never inserted).

DocumentNotFoundError

If no document with this id exists in the table.

Source code in src/tinydantic/_model.py
def replace(self) -> None:
    """Overwrite this model's stored document in place.

    Requires ``id`` to be set. Unlike
    [update][tinydantic.TinydanticModel.update], which merges
    fields, ``replace`` swaps the entire stored document for this
    model's current serialized state, so fields absent from the
    model are removed. Unlike
    [save][tinydantic.TinydanticModel.save], which re-inserts a
    missing document, ``replace`` requires the document to already
    exist.

    Raises:
        DocumentIDRequiredError: If ``id`` is not set (the model
            was never inserted).
        DocumentNotFoundError: If no document with this ``id``
            exists in the table.
    """
    if self.id is None:
        raise DocumentIDRequiredError(
            model_name=type(self).__name__,
            operation="replace",
        )

    try:
        updated_doc_ids = self.get_table().update(
            # In TinyDB, the Table.update/update_multiple methods
            # currently annotate the fields parameter with the type
            # Callable[[Mapping], None].
            #
            # However, the doc parameter that is passed to this
            # transform function is actually a python dict (which
            # is a type of MutableMapping).
            #
            # This cast is simply a band-aid to keep the type
            # checker happy.
            #
            # TODO @cdwilson: remove this cast once the annotation
            # is fixed in TinyDB.
            cast(
                "Callable[[Mapping], None]",
                replace(self.to_tinydb_document(force_dict=True)),
            ),
            doc_ids=[self.id],
        )
    except KeyError:
        raise DocumentNotFoundError(
            model_name=type(self).__name__,
            table_name=self.get_table().name,
            doc_id=self.id,
        ) from None

    if not updated_doc_ids:
        raise DocumentNotFoundError(
            model_name=type(self).__name__,
            table_name=self.get_table().name,
            doc_id=self.id,
        )

save ¤

save() -> Self

Insert this model if it is new, otherwise update it by id.

If id is set but the document no longer exists in the table, it is re-inserted under the same id (TinyDB upsert semantics) — unlike replace()/delete(), which raise DocumentNotFoundError.

Returns:

Type Description
Self

This instance (with id set if it was newly inserted).

Source code in src/tinydantic/_model.py
def save(self) -> Self:
    """Insert this model if it is new, otherwise update it by id.

    If ``id`` is set but the document no longer exists in the
    table, it is re-inserted under the same id (TinyDB upsert
    semantics) — unlike ``replace()``/``delete()``, which raise
    [DocumentNotFoundError][tinydantic.DocumentNotFoundError].

    Returns:
        This instance (with ``id`` set if it was newly inserted).
    """
    if self.id is None:
        return self.insert()
    self.id = self.get_table().upsert(self.to_tinydb_document())[0]
    return self

search classmethod ¤

search(cond: QueryLike) -> list[Self]

Get all documents matching cond as validated models.

Source code in src/tinydantic/_model.py
@classmethod
def search(cls, cond: QueryLike) -> list[Self]:
    """Get all documents matching ``cond`` as validated models."""
    return [
        cls.from_tinydb_document(doc)
        for doc in cls.get_table().search(cond)
    ]

to_tinydb_document ¤

to_tinydb_document(
    *, force_dict: bool = False
) -> dict[str, Any] | Document

Convert this model to a TinyDB-storable document.

Uses JSON-mode serialization so rich pydantic types (datetime, UUID, enums, nested models, ...) become JSON-safe primitives that round-trip through any TinyDB storage. The id field is never embedded in the document — it maps to TinyDB's doc_id.

Parameters:

Name Type Description Default
force_dict bool

Return a plain dict even when id is set (otherwise a Document carrying doc_id is returned).

False
Source code in src/tinydantic/_model.py
def to_tinydb_document(
    self,
    *,
    force_dict: bool = False,
) -> dict[str, Any] | Document:
    """Convert this model to a TinyDB-storable document.

    Uses JSON-mode serialization so rich pydantic types (datetime,
    UUID, enums, nested models, ...) become JSON-safe primitives
    that round-trip through any TinyDB storage. The ``id`` field is
    never embedded in the document — it maps to TinyDB's
    ``doc_id``.

    Args:
        force_dict: Return a plain dict even when ``id`` is set
            (otherwise a [Document][tinydb.table.Document] carrying
            ``doc_id`` is returned).
    """
    doc = self.model_dump(mode="json", exclude={"id"})

    if (force_dict is False) and (self.id is not None):
        doc = Document(doc, self.id)

    return doc

truncate classmethod ¤

truncate() -> None

Remove every document from the table.

Delegates to tinydb.table.Table.truncate, leaving the table empty and resetting its document id counter.

Source code in src/tinydantic/_model.py
@classmethod
def truncate(cls) -> None:
    """Remove every document from the table.

    Delegates to [tinydb.table.Table.truncate][], leaving the table
    empty and resetting its document id counter.
    """
    cls.get_table().truncate()

update classmethod ¤

update(
    fields: Mapping | Callable[[Mapping], None],
    cond: QueryLike | None = None,
    *,
    doc_ids: Iterable[int] | None = None
) -> list[int]

Update matching documents with new fields or a transform.

A fields mapping gets the same treatment insert() and save() give whole models: each value that belongs to a model field is validated against that field's type and serialized to a JSON-safe primitive before it reaches storage (keys that are not model fields pass through unchanged). A transform callable is handed to TinyDB as-is — what it writes is up to you.

Returns:

Type Description
list[int]

The ids of all updated documents.

Raises:

Type Description
ValidationError

If a mapping value fails validation against its field's type.

Source code in src/tinydantic/_model.py
@classmethod
def update(
    cls,
    fields: Mapping | Callable[[Mapping], None],
    cond: QueryLike | None = None,
    *,
    doc_ids: Iterable[int] | None = None,
) -> list[int]:
    """Update matching documents with new fields or a transform.

    A ``fields`` mapping gets the same treatment ``insert()`` and
    ``save()`` give whole models: each value that belongs to a
    model field is validated against that field's type and
    serialized to a JSON-safe primitive before it reaches storage
    (keys that are not model fields pass through unchanged). A
    transform callable is handed to TinyDB as-is — what it writes
    is up to you.

    Returns:
        The ids of all updated documents.

    Raises:
        pydantic.ValidationError: If a mapping value fails
            validation against its field's type.
    """
    if not callable(fields):
        fields = cls._serialize_update_fields(fields)
    return cls.get_table().update(
        # See replace() for why this cast is needed.
        # TODO @cdwilson: remove this cast once the annotation is
        # fixed in TinyDB.
        cast("Callable[[Mapping], None]", fields),
        cond=cond,
        doc_ids=doc_ids,
    )

update_multiple classmethod ¤

update_multiple(
    updates: Iterable[
        tuple[
            Mapping | Callable[[Mapping], None], QueryLike
        ]
    ],
) -> list[int]

Apply several (fields_or_transform, cond) updates at once.

Each update's fields mapping is validated and serialized exactly as in update; transform callables pass through to TinyDB as-is.

Returns:

Type Description
list[int]

The ids of all updated documents.

Raises:

Type Description
ValidationError

If a mapping value fails validation against its field's type.

Source code in src/tinydantic/_model.py
@classmethod
def update_multiple(
    cls,
    updates: Iterable[
        tuple[
            Mapping | Callable[[Mapping], None],
            QueryLike,
        ]
    ],
) -> list[int]:
    """Apply several (fields_or_transform, cond) updates at once.

    Each update's fields mapping is validated and serialized
    exactly as in [update][tinydantic.TinydanticModel.update];
    transform callables pass through to TinyDB as-is.

    Returns:
        The ids of all updated documents.

    Raises:
        pydantic.ValidationError: If a mapping value fails
            validation against its field's type.
    """
    prepared = [
        (
            fields
            if callable(fields)
            else cls._serialize_update_fields(fields),
            cond,
        )
        for fields, cond in updates
    ]
    return cls.get_table().update_multiple(
        # See replace() for why this cast is needed.
        cast(
            "Iterable[tuple[Callable[[Mapping], None], QueryLike]]",
            prepared,
        ),
    )

upsert classmethod ¤

upsert(
    document: Self, cond: QueryLike | None = None
) -> list[int]

Update documents matching cond, or insert document.

Returns:

Type Description
list[int]

The ids of the updated (or inserted) documents.

Source code in src/tinydantic/_model.py
@classmethod
def upsert(
    cls,
    document: Self,
    cond: QueryLike | None = None,
) -> list[int]:
    """Update documents matching ``cond``, or insert ``document``.

    Returns:
        The ids of the updated (or inserted) documents.
    """
    return cls.get_table().upsert(
        document.to_tinydb_document(force_dict=cond is not None),
        cond,
    )

TinydanticUserError ¤

Bases: TinydanticError

Base class for errors caused by incorrect use of tinydantic.

Source code in src/tinydantic/_errors.py
class TinydanticUserError(TinydanticError):
    """Base class for errors caused by incorrect use of tinydantic."""

q ¤

q(field: Any) -> Query

Build a typed TinyDB Query from a field or a field name.

At runtime, class-level field access like User.name already returns a Query (courtesy of the model metaclass), but static type checkers see the field annotation instead, so User.name == "Alice" types as bool. Wrapping the field in q() gives the type checker the runtime truth:

User.search(q(User.name) == "Alice")

A string builds a query on that document key (tinydb.queries.where). This is the escape hatch for fields whose names collide with model methods (search, get, count, ...) and are therefore unreachable through the Model.field shorthand:

Command.search(q("search") == "fuzzy")

Parameters:

Name Type Description Default
field Any

A class-level field expression (e.g. User.name) or a field name string (e.g. "name").

required

Returns:

Type Description
Query

The field expression unchanged, or a Query on the named

Query

field — either way, typed as a Query.

Raises:

Type Description
TypeError

If field is neither a TinyDB Query nor a string — for example when called with an instance attribute instead of class-level field access.

Source code in src/tinydantic/_model.py
def q(field: Any) -> Query:
    """Build a typed TinyDB Query from a field or a field name.

    At runtime, class-level field access like ``User.name`` already
    returns a [Query][tinydb.queries.Query] (courtesy of the model
    metaclass), but static type checkers see the field annotation
    instead, so
    ``User.name == "Alice"`` types as ``bool``. Wrapping the field in
    ``q()`` gives the type checker the runtime truth:

    ```python
    User.search(q(User.name) == "Alice")
    ```

    A string builds a query on that document key
    (``tinydb.queries.where``). This is the escape hatch for fields
    whose names collide with model methods (``search``, ``get``,
    ``count``, ...) and are therefore unreachable through the
    ``Model.field`` shorthand:

    ```python
    Command.search(q("search") == "fuzzy")
    ```

    Args:
        field: A class-level field expression (e.g. ``User.name``)
            or a field name string (e.g. ``"name"``).

    Returns:
        The field expression unchanged, or a Query on the named
        field — either way, typed as a Query.

    Raises:
        TypeError: If ``field`` is neither a TinyDB Query nor a
            string — for example when called with an instance
            attribute instead of class-level field access.
    """
    if isinstance(field, str):
        return where(field)
    if not isinstance(field, Query):
        msg = (
            f"q() expected a TinyDB Query (class-level field access "
            f"like Model.field) or a field name string, got "
            f"{type(field).__name__!r}"
        )
        raise TypeError(msg)
    return field