Skip to content

Record the flat-namespace decision and publish the reserved-name list #121

Open

Description

@cdwilson

Record the flat-namespace decision in the docs, and publish the reserved-name list.

What happens

tinydantic puts every operation directly on the model class (Beanie-style), so any field whose name collides with a method is refused at class definition with ShadowedFieldError. Walking the MRO exactly as the shadow check does, the reserved set is now 60 names:

after_read, all, before_write, bind, clear_cache, construct, contains, copy,
count, delete, dict, find, from_orm, from_tinydb_document, get, get_by_id,
get_by_ids, get_database, get_or_none, get_table, insert, insert_many, json,
model_computed_fields, model_config, model_construct, model_copy, model_dump,
model_dump_json, model_extra, model_fields, model_fields_set,
model_json_schema, model_parametrized_name, model_post_init, model_rebuild,
model_validate, model_validate_json, model_validate_strings, parse_file,
parse_obj, parse_raw, patch, remove, remove_by_ids, replace, save, schema,
schema_json, search, to_tinydb_document, truncate, unbind, update, update_all,
update_by_ids, update_forward_refs, update_many, upsert, validate

Among them: all, count, search, copy, json, schema, update, validate, delete, get, find, replace, save, remove. Those are ordinary domain field names — a SearchIndex with search: str, a Report with count: int, a Webhook with json: str are all refused.

Why it matters

The cost is not today's collisions; it is that adding a method is a breaking change for every user. The unreleased changelog is the proof — several BREAKING entries exist purely to reserve new names, and the churn continued after the review: before_save/after_load left and before_write/after_read arrived, insert_multiple became insert_many, get_or_none/update_by_ids/remove_by_ids were added, get_by_cond/get_or_raise were removed. The set went from 59 names to 60 while the review sat.

The handling, given the choice, is excellent: failing loudly at class definition beats pydantic's easily-missed warning, and shadowed_fields= is a clean opt-out. This issue is not asking to change the design — it is asking to write the decision down before 1.0, because after 1.0 it cannot change.

What is missing

  1. The decision itself is unrecorded. Keeping the flat Beanie-style API (and rejecting a Model.docs manager namespace) was decided off-repo; nothing in docs/ states it. It needs the same treatment docs/contributing/static_typing.md gives the q()/Mapped[T] decision: what was chosen, what was rejected and why, and what would re-open it.
  2. The reserved-name list is unpublished. docs/usage/configuration.md states the consequence in prose — "a future tinydantic release that adds a new model method may newly shadow one of your fields" — without ever listing the names. Today a user discovers the list by hitting the error. A generated table turns a surprise into a lookup.

If the list is published it should be generated from the MRO at docs-build time, not hand-maintained, or it will go stale exactly as it did during this review.

Reproduction

The symptom — an ordinary domain field name is refused at class definition:

from tinydb import TinyDB
from tinydb.storages import MemoryStorage

from tinydantic import TinydanticModel

db = TinyDB(storage=MemoryStorage)


class Report(TinydanticModel, database=db):
    count: int

Actual output:

UserWarning: Field name "count" in "Report" shadows an attribute in parent "TinydanticModel"
...
tinydantic._errors.ShadowedFieldError: Field(s) on 'Report' shadow existing attributes: 'count' shadows TinydanticModel.count. Model.field query sugar would silently break for them. Rename the field(s), or declare shadowed_fields=('count',) on the class and query them with field(Report, <field name>).

The full set, walked exactly as __pydantic_init_subclass__() walks it (name in vars(klass) for each class in the MRO):

from tinydantic import TinydanticModel

reserved = {
    name
    for klass in TinydanticModel.__mro__
    for name in vars(klass)
    if not name.startswith("_")
}
print(len(reserved), "reserved names")
print(", ".join(sorted(reserved)))

Actual output:

60 reserved names
after_read, all, before_write, bind, clear_cache, construct, contains, copy, count, delete, dict, find, from_orm, from_tinydb_document, get, get_by_id, get_by_ids, get_database, get_or_none, get_table, insert, insert_many, json, model_computed_fields, model_config, model_construct, model_copy, model_dump, model_dump_json, model_extra, model_fields, model_fields_set, model_json_schema, model_parametrized_name, model_post_init, model_rebuild, model_validate, model_validate_json, model_validate_strings, parse_file, parse_obj, parse_raw, patch, remove, remove_by_ids, replace, save, schema, schema_json, search, to_tinydb_document, truncate, unbind, update, update_all, update_by_ids, update_forward_refs, update_many, upsert, validate

Expected: the same 60 names, published in the docs and generated at build time by this walk, so a user looks the list up instead of discovering it one ShadowedFieldError at a time.

Environment: tinydantic 0.4.0 at 4d373d1, Python 3.10.20, TinyDB 4.9.0, pydantic 2.13.4, macOS. Run 2026-08-13 — still 60 names, unchanged from the count in the body above.


Verified at e7d0ed0 on 2026-08-12. Originally finding M1 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