Skip to content

Mypy inappropriately infers class type as _typeshed.DataclassInstance #14941

Closed

Description

@binishkaspar

Bug Report
In the following code snippet, mypy incorrectly infers the type of typ as _typeshed.DataclassInstance. This issue was not present in previous versions of mypy

To Reproduce

from dataclasses import dataclass, is_dataclass
from typing import TypeVar, Any, Type, cast


@dataclass
class A:
  a: int


T = TypeVar('T')


def parse(typ: Type[T], raw: dict[str, Any]) -> T:
  if not is_dataclass(typ):
    raise Exception('Unsupported type')

  parsed = typ(**raw)  # type: ignore[call-arg]
  return parsed


parse(A, {'a': 2})

Expected Behavior

There should be no type error

Actual Behavior

Incompatible return value type (got "DataclassInstance", expected "T") \[return-value\]

Your Environment

  • Mypy version used: 1.1.1
  • Python version used: 3.11

Activity

  1. erictraut commented on Mar 23, 2023

    @erictraut

    Mypy is narrowing the type of typ to type[DataclassInstance] because the is_dataclass function which was recently changed to use a TypeGuard. This is why the behavior changed with recent versions of mypy. Here's the relevant overload from dataclasses.pyi:

    def is_dataclass(obj: type) -> TypeGuard[type[DataclassInstance]]: ...
  2. gschaffner commented on Mar 23, 2023

    @gschaffner
    Contributor

    i think that narrowing from type[T] to type[DataclassInstance] is problematic becuase DataclassInstance is a Protocol. type[DataclassInstance] is not always a subtype of type[T]1.

    would this be solved if type narrowing used intersections (python/typing#213)?

    Footnotes

    1. depending on the bound on T. if T is unbound then type[DataclassInstance] is a subtype of type[T], but if we had a bound T = TypeVar("T", bound=A) and A wasn't a subtype of DataclassInstance, then type[DataclassInstance] would legitimately not be a subtype of type[T]. ↩

  3. gschaffner commented on Mar 23, 2023

    @gschaffner
    Contributor

    here's a small reproducer that isn't tied to the recent typeshed changes:

    @runtime_checkable
    class _HasFoo(Protocol):
        # some structural type that is third-party private and not available for users to use in annotations.
        @classmethod
        def foo(cls) -> None:
            ...
    
    
    def class_has_foo(typ: type, /) -> TypeGuard[type[_HasFoo]]:
        # some TypeGuard for type[it] that is third-party public.
        return issubclass(typ, _HasFoo)
    
    
    def construct(typ: type[T], /) -> T:
        if not class_has_foo(typ):
            raise TypeError()
    
        return typ()

    (https://mypy-play.net/?mypy=1.1.1&python=3.11&gist=e1d8c7379c14222d2ec5f2e91eca4f62)

    pyright and pyre error with this too.

  4. erictraut commented on Mar 23, 2023

    @erictraut

    would this be solved if type narrowing used intersections

    Yes, intersections would solve this.

    pyright and pyre error with this too.

    I just checked in a fix for pyright that addresses this issue. Pyright has the concept of conditional types, which is effectively an internal intersection between a TypeVar and another type.

  5. binishkaspar commented on Mar 23, 2023

    @binishkaspar
    Author

    I think for now I will introduce a new function guardsafe_is_dataclass (may be need a better name) which returns bool without using TypeGuard

    from dataclasses import dataclass, is_dataclass
    from typing import TypeVar, Any, Type, cast
    
    
    @dataclass
    class A:
      a: int
    
    
    T = TypeVar('T')
    
    
    def guardsafe_is_dataclass(typ: Type[T]) -> bool:
        return is_dataclass(typ)
    
    
    def parse(typ: Type[T], raw: dict[str, Any]) -> T:
      if not guardsafe_is_dataclass(typ):
        raise Exception('Unsupported type')
    
      parsed = typ(**raw)  # type: ignore[call-arg]
      return parsed
    
    
    parse(A, {'a': 2})
  6. dszady-rtbh commented on Dec 5, 2023

    @dszady-rtbh

    Can we expect this to be fixed?

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

    bugmypy got something wrong

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions