Skip to content

A computed field named after a model method silently replaces the method #152

Open

Description

@cdwilson

What happens

The shadow check in __pydantic_init_subclass__() walks cls.model_fields — ordinary fields only. Computed fields live in model_computed_fields, so a @computed_field whose name collides with a model method sails through: no ShadowedFieldError, and no pydantic warning either. _install_computed_field_queries() then wraps the property in a _ComputedFieldQuery descriptor, so class-level access becomes query sugar and the method is gone: Batch.count is a GuardedQuery, and Batch.count() — the classmethod every other model has — raises a TypeError about QueryInstance.__call__().

For an ordinary field this exact collision is a hard error whose message explains that the query sugar would "silently break". The computed-field door is the mirror image — the sugar wins and the method silently breaks — with no diagnostic at all. As far as I can tell this is the only fully silent collision left in the flat namespace, whose design (#121) leans on collisions being loud.

Why it matters

The failure is remote from the cause. The class defines cleanly, instances work (batch.count computes the property), queries work (Batch.count == 4 builds a valid condition) — only a later call to the replaced method fails, with a message that mentions neither the computed field nor shadowing.

What to do

Extend the check to computed fields: for each name in cls.model_computed_fields, flag it when the name appears in vars() of a class in the MRO other than the one that defines the property itself (the defining class legitimately owns that attribute). Whether shadowed_fields= should then cover computed names too — making today's behavior available as an explicit opt-in — can be decided with the fix; the silent default is the bug.

Reproduction

from pydantic import computed_field
from tinydb import TinyDB
from tinydb.storages import MemoryStorage

from tinydantic import TinydanticModel

db = TinyDB(storage=MemoryStorage)


class Batch(TinydanticModel, database=db):
    size: int

    @computed_field  # type: ignore[prop-decorator]
    @property
    def count(self) -> int:
        return self.size


print("class created: no error, no warning")
print("Batch.count is now:", Batch.count)
try:
    Batch.count()
except TypeError as e:
    print("Batch.count() raises:", e)

Run with python repro.py (project venv; stderr included — no warning appears).

Actual output:

class created: no error, no warning
Batch.count is now: GuardedQuery()
Batch.count() raises: QueryInstance.__call__() missing 1 required positional argument: 'value'

Expected: ShadowedFieldError at class definition, exactly as for an ordinary field named count.

Environment: tinydantic 0.4.0 at 389c6d9, Python 3.10.20, TinyDB 4.9.0, pydantic 2.13.4, macOS. Run 2026-08-14.


The reproduction was run as shown. No fix was built or tested. Surfaced while working through the conflict-handling story for #121.

Assisted-by: Claude:claude-fable-5

Activity

  1. added
    bugSomething isn't working
    api-designShape of the public surface
    ai-assistedCreated or investigated with help from an AI agent
    on Aug 14, 2026
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 surfacebugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions