Repository navigation
Description
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
- The decision itself is unrecorded. Keeping the flat Beanie-style API (and rejecting a
Model.docsmanager namespace) was decided off-repo; nothing indocs/states it. It needs the same treatmentdocs/contributing/static_typing.mdgives theq()/Mapped[T]decision: what was chosen, what was rejected and why, and what would re-open it. - The reserved-name list is unpublished.
docs/usage/configuration.mdstates 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: intActual 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
Activity