typeshed icon indicating copy to clipboard operation
typeshed copied to clipboard

`dataclasses.Field.type` isn't always a type

Open beauxq opened this issue 2 years ago • 3 comments

I don't know whether this is a bug in Python or a bug in typeshed, but it's a bug in at least one. (And I'm kind of worried that both are going to say it's the other one with the bug.)

in dataclasses typeshed has this

class Field(Generic[_T]):
    name: str
    type: Type[_T]

But that doesn't always match the Python implementation.

from __future__ import annotations

from dataclasses import dataclass, fields


class B:
    pass


@dataclass
class C:
    b: B


def f() -> None:
    for field in fields(C):
        if issubclass(field.type, B):
            print("found a b field")


if __name__ == "__main__":
    f()

This type checks without any errors, but run time fails with TypeError: issubclass() arg 1 must be a class

field.type is not a type, it's a str I think this is because of from __future__ import annotations

Does typeshed need to change to this?

    type: Type[_T] | str

beauxq avatar Jan 11 '24 15:01 beauxq

Typeshed is wrong here. The type field can also be some other type form that is not actually a type, such as a union object. We should say it is of type Any.

JelleZijlstra avatar Jan 11 '24 16:01 JelleZijlstra

Would Any make it give a type checking error in if issubclass(field.type, B):?

I was hoping for something that would tell me there's a problem with that line.

beauxq avatar Jan 11 '24 16:01 beauxq

Any would not give a type checking error. It is a "please don't complain" marker that lets you do anything without type checking errors. But it is possible to achieve what you want with type[_T] | str, or even better, type[_T] | str | Any. You would get an error message that basically says you need to handle strings.

Unlike you would expect, type[_T] | str | Any isn't same as just using Any. It would give an error for if issubcass(field.type, B) like you want. See "the Any trick" in our CONTRIBUTING.md.

Here's an example of what the .type can be:

>>> from dataclasses import dataclass, fields
>>> @dataclass
... class Foo:
...     a: str
...     b: 'Foo'
...     c: str | int
... 
>>> [f.type for f in fields(Foo)]
[<class 'str'>, 'Foo', str | int]
>>> [type(f.type) for f in fields(Foo)]
[<class 'type'>, <class 'str'>, <class 'types.UnionType'>]

So the fields can be types (Type[_T]), strings (str), or some other special things that would be difficult to describe accurately (Any).

Akuli avatar Jan 11 '24 22:01 Akuli