Skip to content

Mypy inappropriately infers class type as _typeshed.DataclassInstance #14941

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

Closed
binishkaspar opened this issue Mar 23, 2023 · 6 comments · Fixed by #19183
Closed

Mypy inappropriately infers class type as _typeshed.DataclassInstance #14941

binishkaspar opened this issue Mar 23, 2023 · 6 comments · Fixed by #19183
Labels
bug mypy got something wrong

Comments

@binishkaspar
Copy link

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
@binishkaspar binishkaspar added the bug mypy got something wrong label Mar 23, 2023
@erictraut
Copy link

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]]: ...

@gschaffner
Copy link
Contributor

gschaffner commented Mar 23, 2023

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].

@gschaffner
Copy link
Contributor

gschaffner commented Mar 23, 2023

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.

@erictraut
Copy link

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.

@binishkaspar
Copy link
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})

@dszady-rtb
Copy link

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
Labels
bug mypy got something wrong
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants