youtype / mypy_boto3_builder

Type annotations builder for boto3 compatible with VSCode, PyCharm, Emacs, Sublime Text, pyright and mypy.
https://youtype.github.io/mypy_boto3_builder/
MIT License
544 stars 36 forks source link

pyright now treats TypedDict field types as invariant instead of covariant resulting in some type checking errors #232

Closed ITProKyle closed 11 months ago

ITProKyle commented 11 months ago

Describe the bug

Upon updating to pyright@1.1.333, I am seeing a few type errors that I believe relate to the following change:

Fixed a bug that resulted in a false negative when assigning one TypedDict to another TypedDict. Field types should be treated as invariant rather than covariant because they are mutable (unless marked readonly).

This is present when using a paginator (as illustrated below) that has it's own return type (ListResourceRecordSetsResponsePaginatorTypeDef) that differs from the non-paginated response type (ListResourceRecordSetsResponseTypeDef). Digging this is caused by ResourceRecordSetPaginatorTypeDef.ResourceRecords: NotRequired[List[ResourceRecordTypeDef]] vs ResourceRecordSetTypeDef.ResourceRecords: NotRequired[Sequence[ResourceRecordTypeDef]].

There may be other locations, this is just the first one I've run into while going through projects updating pyright.

To Reproduce

  1. Install boto3-stubs[route53]==1.28.73
  2. Install pyright@1.1.333
  3. Run pyright on the file.
"""Example."""
from __future__ import annotations

from typing import TYPE_CHECKING

import boto3

if TYPE_CHECKING:
    from mypy_boto3_route53.type_defs import ChangeTypeDef

HZ_ID = "foo"
R53_CLIENT = boto3.client("route53")

CHANGES: list[ChangeTypeDef] = [
    {"Action": "DELETE", "ResourceRecordSet": record}
    for record in [
        resource_record_set
        for page in R53_CLIENT.get_paginator("list_resource_record_sets").paginate(
            HostedZoneId=HZ_ID
        )
        for resource_record_set in page["ResourceRecordSets"]
        if resource_record_set["Type"] not in ["NS", "SOA"]
    ]
]
R53_CLIENT.change_resource_record_sets(
    HostedZoneId=HZ_ID, ChangeBatch={"Changes": CHANGES}
)
vemel commented 11 months ago

Thank you for the report! I will take a look and let you know.

vemel commented 11 months ago

Okay, I have two good news: the issue was in pyright and it was fixed.

Investigation

While investigating, I found that the following code is enough to reproduce the issue:

from typing import Sequence, TypedDict

class NameSet(TypedDict):
    names: list[str]

class InputNameSet(TypedDict):
    names: Sequence[str]

def convert(name_set: InputNameSet) -> None:
    return None

name_set: NameSet = {
    "names": ["test"],
}
input_name_set: InputNameSet = {
    "names": ["test"],
}

convert(input_name_set)  # works as it should
convert(name_set) # Argument of type "NameSet" cannot be assigned to parameter "name_set" of type "InputNameSet" in function "convert"

So, this is definitely an issue in pyright, because even with invariant fields matching, the code sample has no issues from the logical POV.

Resolution

Obviously, this was fixed in pyright 1.1.334. So, say thanks to @erictraut for a quick fix :)

I tested both your code sample and mine with `pyright 1.1.334, and no issues were found. Let me know if it works for you as it should.

vemel commented 11 months ago

Most likely, it was fixed here: https://github.com/microsoft/pyright/issues/6246

ITProKyle commented 7 months ago

Sorry for the delay. Never got around to checking this out and eventually forgot about it all together. A coworker of mine just ran into this reminding me that I had opened an issue about it.

Anyway, this problem is still present with boto3-stubs==1.34.38 & pyright@1.1.350 (pyright@1.1.334 as well).

erictraut commented 7 months ago

There is a draft PEP 705 that introduces a way to mark TypedDict entries as "read only". This allows them to be treated as covariant.

Here's what that looks like. You can play with it today in pyright if you set enableExperimentalFeatures to true in your pyright configuration.

Code sample in pyright playground

from typing import Sequence, TypedDict
from typing_extensions import ReadOnly

class NameSet(TypedDict):
    names: ReadOnly[list[str]]

class InputNameSet(TypedDict):
    names: ReadOnly[Sequence[str]]