team23 / pydantic-partial

Create partial models from pydantic models

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Partial classes cannot be used in type annotations

jdinunzio opened this issue · comments

Issue

Using python 3.10.7, pydantic 4.3.2, pydantic-partial 0.3.2 and 0.3.3, mypy 0.971

from pydantic import BaseModel
from pydantic_partial import PartialModelMixin

class Foo(PartialModelMixin, BaseModel):
    id: int

PartialFoo = Foo.as_partial()

reveal_type(Foo())
reveal_type(PartialFoo())

def something(x: PartialFoo):
    return x.dict()

running mypy will return:

tmp/foo.py:10: note: Revealed type is "foo.Foo"
tmp/foo.py:11: note: Revealed type is "foo.Foo"
tmp/foo.py:13: error: Variable "foo.PartialFoo" is not valid as a type
tmp/foo.py:13: note: See https://mypy.readthedocs.io/en/stable/common_issues.html#variables-vs-type-aliases
tmp/foo.py:14: error: PartialFoo? has no attribute "dict"

(revealed type for PartialFoo() is "Any" in 0.3.2 and "foo.Foo" in 0.3.3).

Question / Request

How to use partials as type annotations? If there's a way to do it, could it be documented?

I initially thought that adding a real class will fix this, but mypy cannot resolve the type then:

from pydantic import BaseModel
from pydantic_partial import PartialModelMixin

class Foo(PartialModelMixin, BaseModel):
    id: int

class PartialFoo(Foo.as_partial()):
    pass

reveal_type(Foo())
reveal_type(PartialFoo())

def something(x: PartialFoo):
    return x.dict()

...this will still give you test.py:7: error: Unsupported dynamic base class "Foo.as_partial" as an error.

The main issue is, that we are constructing a type during runtime. This dynamic type is not supported by mypy at all.
(see python/mypy#2477 for some background)

I tried to mark PartialModelMixin.as_partial() and create_partial_model(...) to return the same type, so for the type checker the class PartialFoo is basically the same as Foo. This is due to the fact that the dynamically changed type cannot be defined in the Python typing system. Sadly this seems to not be enough for mypy.

You can work around this issue by tricking mypy into not seeing the conversion to partial at all. Note however that this still means all partial model instances will just be seen as instanced of Foo.

See the following example code:

from typing import TYPE_CHECKING

from pydantic import BaseModel
from pydantic_partial import PartialModelMixin

class Foo(PartialModelMixin, BaseModel):
    id: int

if TYPE_CHECKING:
    PartialFoo = Foo
else:
    PartialFoo = Foo.as_partial()

reveal_type(Foo())
reveal_type(PartialFoo())

def something(x: PartialFoo):
    return x.dict()

Which will produce the following mypy output:

test.py:14: note: Revealed type is "test.Foo"
test.py:15: note: Revealed type is "test.Foo"
Success: no issues found in 1 source file

Side note: If anyone knows a better way to solve this or how to define the types in this library, please feel free to send me some suggestions or even a pull request. ;-)

Currently my feeling is I have to write a mypy plugin... 🤷‍♂️🤔

The ideal solution in my mind would look something like

PartialFoo = Partial[Foo]

if only typing.Generic would allow being sub-classed into a meta-class, but sadly that seems not to be an option.

@jdinunzio Yeah, that would be the best syntax. Sadly using __class_getitem__ will confuse mypy and at least PyCharm. But I will see what I can do when thinking about a plugin. Have to read into this first though....so this will probably take some time.

If anyone else is interested in solving this a PR would be very much appreciated. 👍
(but please drop me a note here - as I will do when I start producing some real code)

About Partial[...], see python/mypy#11501
(mypy currently does always expect __class_getitem__ to be used for generic types)

Note: pydantic itself thought about adding partial support, but then decided to not do this for now. Reason is - like with this ticket - that there is no good way to get the typing definition done, as there is no partial equivalent in the python typing system now. As of this I will do the same and kind of ignore the fact that pydantic-partial will not (and kind of cannot) produce partial models in a way type checkers could recognise. There is just no base typing mechanism to support this.

See pydantic/pydantic#1673 (comment) for reference.

Note: I will keep this issue open to have this documented. It still is an open issue - but just one we cannot resolve in a good way.