Skip to content

type builtin function should return an instance of typing.Type? #1758

Closed

Description

@Michael0x2a

I've run into a snippit of code which essentially boils down into this:

class SomeObject(object):
    @classmethod
    def test_class_method(cls) -> None:
        print('Test class method')

    @staticmethod
    def test_static_method() -> None:
        print('Test static method')

s = SomeObject()
t = type(s)

# Doesn't typecheck
t.test_static_method()
t.test_class_method()

The behavior I was expecting was for t to be of type Type[SomeObject] -- instead, it's of type type, due to how type is annotated in Typeshed:

class type:
    __bases__ = ...  # type: Tuple[type, ...]
    __name__ = ...  # type: str
    __qualname__ = ...  # type: str
    __module__ = ...  # type: str
    __dict__ = ...  # type: Dict[str, Any]
    __mro__ = ...  # type: Tuple[type, ...]

    @overload
    def __init__(self, o: object) -> None: ...
    @overload
    def __init__(self, name: str, bases: Tuple[type, ...], dict: Dict[str, Any]) -> None: ...
    @overload
    def __new__(cls, o: object) -> type: ...
    @overload
    def __new__(cls, name: str, bases: Tuple[type, ...], namespace: Dict[str, Any]) -> type: ...
    def __call__(self, *args: Any, **kwds: Any) -> Any: ...
    def __subclasses__(self) -> List[type]: ...
    # Note: the documentation doesn't specify what the return type is, the standard
    # implementation seems to be returning a list.
    def mro(self) -> List[type]: ...

I attempted modifying the definition in Typeshed by first changing __new__ to have the signature def __new__(cls, o: _T) -> Type[_T], but that had no effect. I then tried commenting out both __init__s entirely in case mypy was defaulting to looking at those, but that also did nothing. I'm not entirely sure why the change to __new__ isn't working, though it may be related to issue 1020?

I did find a workaround for this particular case by doing:

from typing import cast, Type

# ...snip...

s = SomeObject()
t2 = cast(Type[SomeObject], type(s))

# typechecks
t2.test_static_method()
t2.test_class_method()

...but it would be nice if type could be changed so this casting is unnecessary, though I'm not entirely sure how this would be done.

Activity

  1. gvanrossum commented on Jun 28, 2016

    @gvanrossum
    Member

    I think mypy would have to special-case type(x) to return Type[C] where
    C is what it knows of the type of x. The same should apply for
    x.__class__. Previously such types were hard to express. They're still a
    little cumbersome internally in mypy but at least they're possible.

    Are you interested in trying to fix this? I expect complications in the
    case what's known about x is not a simple class but a union or type
    variable.

    On Mon, Jun 27, 2016 at 9:08 PM, Michael Lee notifications@github.com
    wrote:

    I've run into a snippit of code which essentially boils down into this:

    class SomeObject(object):
    @classmethod
    def test_class_method(cls) -> None:
    print('Test class method')

    @staticmethod
    def test_static_method() -> None:
        print('Test static method')
    

    s = SomeObject()
    t = type(s)

    Doesn't typecheck

    t.test_static_method()
    t.test_class_method()

    The behavior I was expecting was for t to be of type Type[SomeObject] --
    instead, it's of type type, due to how type is annotated in Typeshed:

    class type:
    bases = ... # type: Tuple[type, ...]
    name = ... # type: str
    qualname = ... # type: str
    module = ... # type: str
    dict = ... # type: Dict[str, Any]
    mro = ... # type: Tuple[type, ...]

    @overload
    def __init__(self, o: object) -> None: ...
    @overload
    def __init__(self, name: str, bases: Tuple[type, ...], dict: Dict[str, Any]) -> None: ...
    @overload
    def __new__(cls, o: object) -> type: ...
    @overload
    def __new__(cls, name: str, bases: Tuple[type, ...], namespace: Dict[str, Any]) -> type: ...
    def __call__(self, *args: Any, **kwds: Any) -> Any: ...
    def __subclasses__(self) -> List[type]: ...
    # Note: the documentation doesn't specify what the return type is, the standard
    # implementation seems to be returning a list.
    def mro(self) -> List[type]: ...
    

    I attempted modifying the definition in Typeshed by first changing new
    to have the signature def new(cls, o: _T) -> Type[_T], but that had
    no effect. I then tried commenting out both __init__s entirely in case
    mypy was defaulting to looking at those, but that also did nothing. I'm not
    entirely sure why the change to new isn't working, though it may be
    related to issue 1020 #1020?

    I did find a workaround for this particular case by doing:

    from typing import cast, Type

    ...snip...

    s = SomeObject()
    t2 = cast(Type[SomeObject], type(s))

    typechecks

    t2.test_static_method()
    t2.test_class_method()

    ...but it would be nice if type could be changed so this casting is
    unnecessary, though I'm not entirely sure how this would be done.

    —
    You are receiving this because you are subscribed to this thread.
    Reply to this email directly, view it on GitHub
    #1758, or mute the thread
    https://github.com/notifications/unsubscribe/ACwrMkroNqtnnjor0B3LAjrUiAvc_QtSks5qQJ5AgaJpZM4I_uT-
    .

    --Guido van Rossum (python.org/~guido)

  2. Michael0x2a commented on Jun 29, 2016

    @Michael0x2a
    CollaboratorAuthor

    (For the record: I've talked to @gvanrossum offline and am currently working on this)

  3. ddfisher commented on Jun 29, 2016

    @ddfisher
    Collaborator

    Is there some reason we shouldn't make the general case of def f(x: T) -> Type[T] work?

  4. rwbarton commented on Jun 29, 2016

    @rwbarton
    Contributor

    I would expect it to work in general, but maybe the problem is that type isn't a normal function, it's the constructor of a class? Does it make sense to make type a generic class?

  5. gvanrossum commented on Jun 29, 2016

    @gvanrossum
    Member

    Yes general case already works (it's the reason Type[T] exists :-).

    The definition of type() is complicated because it overloads a class and a function. The 1-arg version type(x) is a function that is roughly equivalent to x.__class__ (the "roughly" is because a metaclass can override __class__ but not type()). But type is also a class. The 3-arg version instantiates that class, and metaclasses are defined by subclassing it.

    I don't think we can express this fully by clever overloads in a stub; mypy must understand that type(x) doesn't just return an instance of type; it returns a value whose type is Type[X] where X is the type of the argument x. Ditto for x.__class__. (IMO mypy should treat these two the same, despite the possible difference. There are very few actual use cases for overloading __class__; it was allowed to enable extension modules to create perfect proxy classes.)

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

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions