Skip to content

Dataclass "init" method should expect the type from a custom descriptor's "set" method #13856

Closed

Description

@pappasam

This issue is copied, mostly verbatim, from here: microsoft/pyright#3245.

If a @dataclass field is initialized with a descriptor as a default value, mypy should use the value type of the __set__ method for the descriptor when determining the type of the corresponding parameter within the synthesized __init__ method.

Types are also evaluated incorrectly in this case.

from dataclasses import dataclass
from typing import Any, cast


class MyDescriptor:
    def __get__(self, __obj: object | None, __owner: Any) -> "int | MyDescriptor":
        if __obj is None:
            return self
        return cast(Any, __obj)._x

    def __set__(self, __obj: object | None, __value: int) -> None:
        if __obj is not None:
            cast(Any, __obj)._x = __value


@dataclass
class Foo:
    y: MyDescriptor = MyDescriptor()


f = Foo(3)  # Currently a false positive error in mypy

print(f.y)  # 3
f.y = 4
print(f.y)  # 4
print(Foo.y)  # MyDescriptor object

reveal_type(f.y) # int | MyDescriptor, which is wrong
reveal_type(Foo.y) # int | MyDescriptor, which is wrong

To Reproduce

Run mypy against the above script.

Expected Behavior

No Errors

Actual Behavior

test.py:23: error: Argument 1 to "Foo" has incompatible type "int"; expected "MyDescriptor"
test.py:30: note: Revealed type is "Union[builtins.int, test.MyDescriptor]"
test.py:31: note: Revealed type is "Union[builtins.int, test.MyDescriptor]"
Found 1 error in 1 file (checked 1 source file)

Your Environment

  • OS: Ubuntu 22.04.1
  • Mypy version used: 0.982
  • Mypy command-line flags: None
  • Mypy configuration options from mypy.ini (and other config files): None
  • Python version used: 3.10.6

Activity

  1. pappasam commented on Oct 11, 2022

    @pappasam
    Author

    @erictraut fyi, I migrated your issue from pyright here

  2. debonte commented on Mar 10, 2023

    @debonte

    FYI, the behavior of dataclass with descriptor-typed fields was discussed in python/cpython#91330 and is documented here.

  3. glyph commented on Mar 10, 2023

    @glyph

    I think this confusion goes a bit deeper than __set__; Mypy seems to have the wrong idea about dataclass-like types in general non-dataclass PEP 681 classes such as attrs. Consider this brief comparison of attrs and dataclasses:

    from __future__ import annotations
    from typing import reveal_type, TYPE_CHECKING
    from attr import define
    
    class DecoGet:
        pass
    
    class Deco:
        def __set__(self, oself: object, value: Deco) -> None:
            ...
        def __get__(self, oself: object, owner: None | type[object]=None) -> DecoGet:
            return DecoGet()
    
    @define
    class AttrBlub:
        a: Deco
        b: Deco = Deco()
    
    
    from dataclasses import dataclass
    
    @dataclass
    class DataBlub:
        a: Deco
        b: Deco = Deco()
    
    reveal_type(AttrBlub(Deco()).a)
    reveal_type(AttrBlub(Deco()).b)
    
    reveal_type(DataBlub(Deco()).a)
    reveal_type(DataBlub(Deco()).b)
    
    reveal_type(AttrBlub.a)
    reveal_type(AttrBlub.b)
    
    try:
        reveal_type(DataBlub.a)     # AttributeError at runtime
    except:
        print("skipped")
    reveal_type(DataBlub.b)

    Side-by-side, type-check and output looks like this (emojis added to indicate agreement / disagreement between type-time and runtime):

    t.py:27: note: Revealed type is "t.DecoGet"    Runtime type is 'Deco'              ❌ attrs / instance / no default
    t.py:28: note: Revealed type is "t.DecoGet"    Runtime type is 'Deco'              ❌ attrs / instance / default
    t.py:30: note: Revealed type is "t.Deco"       Runtime type is 'Deco'              ✅ dataclasses / instance / no default
    t.py:31: note: Revealed type is "t.DecoGet"    Runtime type is 'DecoGet'           ✅ dataclasses / instance / default
    t.py:33: note: Revealed type is "t.DecoGet"    Runtime type is 'member_descriptor' ❌ attrs / class / no default
    t.py:34: note: Revealed type is "t.DecoGet"    Runtime type is 'member_descriptor' ❌ attrs / class / default
    t.py:37: note: Revealed type is "t.DecoGet"    skipped                             ❌ dataclasses / class / no default
    t.py:40: note: Revealed type is "t.DecoGet"    Runtime type is 'DecoGet'           ✅ dataclasses / class / default
    

    Updated to include annotations about which one's which, which shows that the problem is with attrs' annotations more than it is with dataclasses, although "attribute error" vs. descriptor-type is not great.

  4. glyph commented on Mar 10, 2023

    @glyph

    As I was playing with this I discovered a more specific problem in this neighborhood with Callable #14869

  5. added a commit that references this issue on Mar 12, 2023
  6. added a commit that references this issue on Mar 13, 2023
  7. pappasam commented on Apr 18, 2023

    @pappasam
    Author

    Just tested out with mypy 1.2 and this appears to have been resolved!

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

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions