Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Possible false positive calling dict.update() method with TypedDict hierarchy #9335

Closed
james-perretta opened this issue Aug 21, 2020 · 3 comments

Comments

@james-perretta
Copy link

Python version: 3.8.2
mypy version: 0.782
mypy flags: --strict (same result without this flag)

I've encountered what looks like a false positive when calling .update() on TypedDicts that have some required and some optional fields. I have a hierarchy of TypedDicts with some required fields in the base class and some optional fields in the derived class. Here is a minimal snippet that reproduces the error:

from typing import TypedDict


class Base(TypedDict):
    id: int


class Derived(Base, total=False):
    description: str


update_me: Derived = {
    'id': 1,
    'description': 'norsetanrst'
}

new_data: Derived = {
    'id': 2
}
# "incompatible type" error from mypy
update_me.update(new_data)

I expected this to type check with no errors. However, mypy emits the following about the last line:

$ mypy --strict
demo.py:21: error: Argument 1 to "update" of "TypedDict" has incompatible type "Derived"; expected "TypedDict({'content'?: Dict[str, int], 'description'?: str})"
Found 1 error in 1 file (checked 1 source file)

It seems like mypy is expecting a TypedDict with the same fields (but all optional) as Derived. If I change the last line to update_me.update(update_me), the same error occurs. If I add total=False to Base's signature, the error goes away, but then I no longer have any required fields.

Also interesting to note: reveal_type(update_me.update) prints the signature I would expect:

note: Revealed type is 'def (TypedDict('wee.Derived', {'id': builtins.int, 'description'?: builtins.str}))'

And if I inline the argument, i.e. update_me.update({'id': 2}), the error goes away. However, my use case is
such that doing so is not a viable solution.

Thank you! Please let me know if there's anything I've missed.

@13k
Copy link

13k commented Nov 24, 2020

I'm having a similar issue, regarding updating a child type instance with data from a parent type instance.

I'm not entirely sure it should work and I'm a little confused.

PEP 589 says this on inheritance:

class Movie(TypedDict):
    name: str
    year: int

class BookBasedMovie(Movie):
   based_on: str

It is equivalent to this definition, since TypedDict types use structural compatibility:

class BookBasedMovie(TypedDict):
    name: str
    year: int
    based_on: str

What I understand from this is that given two TypedDict types S and T, where S fields is a superset of T fields, then, by definition, S is update-able with T. S being a superset either via inheritance or not.

So I tested this:

from typing import TypedDict


class BaseRequired(TypedDict):
    x: int


class BaseOptional(TypedDict, total=False):
    x: int


class DerivedRequired_BaseRequired(BaseRequired):
    y: int


class DerivedRequired_BaseOptional(BaseOptional):
    y: int


class DerivedOptional_BaseRequired(BaseRequired, total=False):
    y: int


class DerivedOptional_BaseOptional(BaseOptional, total=False):
    y: int


base_required: BaseRequired = {"x": 1}
base_optional: BaseOptional = {"x": 1}
derived_required__base_required: DerivedRequired_BaseRequired = {"x": 1, "y": 2}
derived_required__base_optional: DerivedRequired_BaseOptional = {"x": 1, "y": 2}
derived_optional__base_required: DerivedOptional_BaseRequired = {"x": 1, "y": 2}
derived_optional__base_optional: DerivedOptional_BaseOptional = {"x": 1, "y": 2}

derived_required__base_required.update(base_required)
derived_required__base_optional.update(base_required)
derived_optional__base_required.update(base_required)
derived_optional__base_optional.update(base_required)

derived_required__base_required.update(base_optional)
derived_required__base_optional.update(base_optional)
derived_optional__base_required.update(base_optional)
derived_optional__base_optional.update(base_optional)


class SmallRequired(TypedDict):
    x: int


class SmallOptional(TypedDict, total=False):
    x: int


class BigRequired(TypedDict):
    x: int
    y: int


class BigOptional(TypedDict, total=False):
    x: int
    y: int


small_required: SmallRequired = {"x": 1}
small_optional: SmallOptional = {"x": 1}
big_required: BigRequired = {"x": 1, "y": 2}
big_optional: BigOptional = {"x": 1, "y": 2}

big_required.update(small_required)
big_required.update(small_optional)

big_optional.update(small_required)
big_optional.update(small_optional)

All calls to update() are reported as errors by mypy:

$ mypy --version
mypy 0.790
$ mypy --no-incremental tdict.py
tdict.py:35: error: Argument 1 to "update" of "TypedDict" has incompatible type "BaseRequired"; expected "TypedDict({'x'?: int, 'y'?: int})"
tdict.py:36: error: Argument 1 to "update" of "TypedDict" has incompatible type "BaseRequired"; expected "TypedDict({'x'?: int, 'y'?: int})"
tdict.py:37: error: Argument 1 to "update" of "TypedDict" has incompatible type "BaseRequired"; expected "TypedDict({'x'?: int, 'y'?: int})"
tdict.py:38: error: Argument 1 to "update" of "TypedDict" has incompatible type "BaseRequired"; expected "TypedDict({'x'?: int, 'y'?: int})"
tdict.py:40: error: Argument 1 to "update" of "TypedDict" has incompatible type "BaseOptional"; expected "TypedDict({'x'?: int, 'y'?: int})"
tdict.py:41: error: Argument 1 to "update" of "TypedDict" has incompatible type "BaseOptional"; expected "TypedDict({'x'?: int, 'y'?: int})"
tdict.py:42: error: Argument 1 to "update" of "TypedDict" has incompatible type "BaseOptional"; expected "TypedDict({'x'?: int, 'y'?: int})"
tdict.py:43: error: Argument 1 to "update" of "TypedDict" has incompatible type "BaseOptional"; expected "TypedDict({'x'?: int, 'y'?: int})"
tdict.py:69: error: Argument 1 to "update" of "TypedDict" has incompatible type "SmallRequired"; expected "TypedDict({'x'?: int, 'y'?: int})"
tdict.py:70: error: Argument 1 to "update" of "TypedDict" has incompatible type "SmallOptional"; expected "TypedDict({'x'?: int, 'y'?: int})"
tdict.py:72: error: Argument 1 to "update" of "TypedDict" has incompatible type "SmallRequired"; expected "TypedDict({'x'?: int, 'y'?: int})"
tdict.py:73: error: Argument 1 to "update" of "TypedDict" has incompatible type "SmallOptional"; expected "TypedDict({'x'?: int, 'y'?: int})"
Found 12 errors in 1 file (checked 1 source file)

@yiyoascen
Copy link

yiyoascen commented May 20, 2021

I have also a similar issue with a dict .update()
1 from typing import Optional
2 from fastapi import FastAPI
3 from pydantic import BaseModel
4
5
6 class Item(BaseModel):
7 name: str
8 description: Optional[str] = None
9 price: float
10 tax: Optional[float] = None
11
12
13 app = FastAPI()
14
15
16 @app.put("/items/{item_id}")
17 async def create_item(item_id: int, item: Item, q: Optional[str] = None):
18 result = {"item_id": item_id, **item.dict()}
19 if q:
20

21 result.update({"q": q})
22 return result

baseModel.py:21: error: Dict entry 0 has incompatible type "str": "str"; expected "str": "int"
Found 1 error in 1 file (checked 1 source file)
It seems to think that it's the first key:value pairs instead of the correct one

@AlexWaygood AlexWaygood added feature and removed bug mypy got something wrong labels May 3, 2022
@AlexWaygood
Copy link
Member

Closing as a duplicate of #6462

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

No branches or pull requests

5 participants