Skip to content

[google-cloud-ndb] Model.get_by_id and get_or_insert should return Self - #16277

Open
miguelsmm wants to merge 1 commit into
python:mainfrom
miguelsmm:ndb-get-by-id-self
Open

[google-cloud-ndb] Model.get_by_id and get_or_insert should return Self#16277
miguelsmm wants to merge 1 commit into
python:mainfrom
miguelsmm:ndb-get-by-id-self

Conversation

@miguelsmm

@miguelsmm miguelsmm commented Aug 21, 2026

Copy link
Copy Markdown

Model.get_by_id and Model.get_or_insert are declared as returning the base class, so every attribute declared on an ndb.Model subclass is reported as missing on correct code.

from google.cloud import ndb


class Challenge(ndb.Model):
    ceremony = ndb.StringProperty(required=True)

    @classmethod
    def get_valid(cls, challenge_id: int):
        entity = cls.get_by_id(challenge_id)
        if entity is None:
            return None
        return entity.ceremony

With types-google-cloud-ndb 2.4.0.20260724 on Python 3.12:

mypy 2.3.1:      repro.py:12: error: "Model" has no attribute "ceremony"  [attr-defined]
pyright 1.1.411: repro.py:12:23 - error: Cannot access attribute "cere

After this change, on the same reproduction:

mypy 2.3.1:      Success: no issues found in 1 source file
pyright 1.1.411: 0 errors

Writing Challenge.get_by_id(...) in place of cls.get_by_id(...) produces the same error before the change.

Why Self is the right annotation

Line references below are google-cloud-ndb 2.5.1.

get_or_insert constructs the entity itself. Model._get_or_insert, aliased at model.py:6075, calls entity = _cls(**kwargs) on the insert path (line 6168) and
otherwise fetches by a key derived from _cls.

get_by_id is one step removed. Model._get_by_id, aliased at models shorthand for ``Key(cls, id, ....).get()`` (line 5859) and delegates to _get_by_id_async, which builds Key(cls._get_kind(), id)and fet What comes back is an instance of the model class registered for thatkind. For an ordinaryModelsubclass the kind is the class name and the registry maps it back to the same class, so the result iscls`.

Neither Model nor its stub defines __getattr__, so nothing softens the missing attributes today.

PolyModel

Self is not exact for polymodel.PolyModel. PolyModel._get_kind dot of the hierarchy (polymodel.py:212), and PolyModel inherits both methods unchanged in the runtime and in polymodel.pyi. So with Animal -> Feline -> Cat, all three share the kind Animal, and Cat.get_by_id(...) can return a Feline.

I think Self is still the better annotation. Model is imprecise for every subclass, which is the common case, while Self is imprecise only for a PolyModel subclass called on a non-root class. If you would rather pin that case override the two methods to return PolyModel, and I am happy to addthat here.

Scope

The cls: type[Model] annotations have to go for Self to bind, but only on these two methods. The _async variants keep theirs: they return tasklets_module.Future, which carries no element type. The remainingters sit on gql, query and allocate_ids, which do not return
entities.

PyCharm bundles this stub, so the false positive also reaches users who never installed types-google-cloud-ndb.

@github-actions

Copy link
Copy Markdown
Contributor

According to mypy_primer, this change has no effect on the checked open source code. 🤖🎉

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant