Skip to content

[1.16 regression] narrowing on is_dataclass #19139

Closed

Description

@henryiii

Bug Report

If you use isinstance(item, type), this used to be used in conjunction with dataclasses.is_dataclass to identify types and instances. It's the recommendation in the CPython documentation, in fact: https://docs.python.org/3/library/dataclasses.html#dataclasses.is_dataclass. However, the latests MyPy no longer narrows this correctly (from what I can tell, unless I'm doing something wrong), and still thinks I might have an instance, which then breaks later when creating an instance, etc.

Note that I can flip the order, in which case I just get builtins.type for both versions. This isn't enough to fix it for some of the more complex usages in scikit-build-core, though.

To Reproduce

import dataclasses

raw_target: object

if isinstance(raw_target, type) and dataclasses.is_dataclass(raw_target):
    reveal_type(raw_target)

Expected Behavior

This is from 1.15:

tmp.py:6: note: Revealed type is "Type[_typeshed.DataclassInstance]"

Actual Behavior

tmp.py:6: note: Revealed type is "Union[_typeshed.DataclassInstance, Type[_typeshed.DataclassInstance]]"

Your Environment

  • Mypy version used: uvx --from git+https://github.com/python/mypy mypy tmp.py was used, so latest commit, also latest release 1.15 version for comparison.
  • Python version used: 3.13

Noticed in hauntsaninja/mypy_primer#174.

Activity

  1. sterliakov commented on May 27, 2025

    @sterliakov
    Collaborator
    This is a regression, and a rather bad one. Note also that this is somehow dependent on checks order. MRE without stdlib dependency:
    from typing import TypeIs
    
    class A: ...
    
    def is_a(o: object) -> TypeIs[type[A] | A]:
        return isinstance(o, int)
    
    raw_target: object
    
    if is_a(raw_target) and isinstance(raw_target, type):
        reveal_type(raw_target)  # N: Revealed type is "builtins.type"
    if isinstance(raw_target, type) and is_a(raw_target):
        reveal_type(raw_target)  # N: Revealed type is "Union[type[__main__.A], __main__.A]"

    https://mypy-play.net/?mypy=master&python=3.13&flags=strict&gist=6e2a57a00f5aa275ebbb2abbefa972e6

    My MRE exhibits the same behaviour on 1.15.0, so it isn't the same. And this is correct - A and type can have a common LSP-compatible subclass.

    This issue bisects to #17678; is_dataclass is an overloaded function:

    # HACK: `obj: Never` typing matches if object argument is using `Any` type.
    @overload
    def is_dataclass(obj: Never) -> TypeIs[DataclassInstance | type[DataclassInstance]]: ...  # type: ignore[narrowed-type-not-subtype]  # pyright: ignore[reportGeneralTypeIssues]
    @overload
    def is_dataclass(obj: type) -> TypeIs[type[DataclassInstance]]: ...
    @overload
    def is_dataclass(obj: object) -> TypeIs[DataclassInstance | type[DataclassInstance]]: ...

    And this looks like a bug - after first isinstance we should have raw_target already narrowed to type, so the second overload should match.

  2. sterliakov commented on May 27, 2025

    @sterliakov
    Collaborator

    @hauntsaninja could you please create a label for PEP 742 / TypeIs?

  3. sterliakov commented on May 28, 2025

    @sterliakov
    Collaborator

    I get it now. When find_isinstance_check_helper is applied, we check all arguments at once, which means wrong @overload is selected - mypy only stores narrowed types when all typemaps are known. cc @sobolevn as original author of #17678 - this sounds like a painful limitation, any overload resolution here won't take preceding narrowing statements into account.

    Splitting the ladder works:

    import dataclasses
    
    raw_target: object
    
    if isinstance(raw_target, type): 
        if dataclasses.is_dataclass(raw_target):
            reveal_type(raw_target)  # N: Revealed type is "type[_typeshed.DataclassInstance]"    
  4. changed the title [-]Regression in 1.16 on DataclassInstance?[/-] [+][1.16 regression] narrowing on is_dataclass[/+] on May 28, 2025
  5. hauntsaninja commented on Jun 3, 2025

    @hauntsaninja
    Collaborator

    Looks like PR causing regression was reverted. #19214 adds a regression test on master

  6. added a commit that references this issue on Jun 3, 2025
    1f339c0
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 wrongtopic-typeguard-typeisTypeGuard / TypeIs / PEP 647 / PEP 742

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions