213

I want to allow type hinting using Python 3 to accept sub classes of a certain class. E.g.:

class A:
    pass

class B(A):
    pass

class C(A):
    pass

def process_any_subclass_type_of_A(cls: A):
    if cls == B:
        # do something
    elif cls == C:
        # do something else

Now when typing the following code:

process_any_subclass_type_of_A(B)

I get an PyCharm IDE hint 'Expected type A, got Type[B] instead.'

How can I change type hinting here to accept any subtypes of A?

According to PEP 484 ("Expressions whose type is a subtype of a specific argument type are also accepted for that argument."), I understand that my solution (cls: A) should work?

Alex Waygood
  • 6,304
  • 3
  • 24
  • 46
user1211030
  • 2,680
  • 2
  • 19
  • 22
  • Is Python also raising this error ? Otherwise, it's maybe a bug in PyCharm ! – jpic Sep 07 '17 at 09:05
  • For me I tried on python console and It is working fine. Might be possible bug in PyCharm.. – Nirmi Sep 07 '17 at 09:13
  • Yes it's working, the type hinting is optional as far as I know. Still I want the user of the method to explicitly know what classes are intended for usage there. – user1211030 Sep 07 '17 at 09:14

3 Answers3

308

When you specify cls: A, you're saying that cls expects an instance of type A. The type hint to specify cls as a class object for the type A (or its subtypes) uses typing.Type.

from typing import Type
def process_any_subclass_type_of_A(cls: Type[A]):
    pass

From The type of class objects :

Sometimes you want to talk about class objects that inherit from a given class. This can be spelled as Type[C] where C is a class. In other words, when C is the name of a class, using C to annotate an argument declares that the argument is an instance of C (or of a subclass of C), but using Type[C] as an argument annotation declares that the argument is a class object deriving from C (or C itself).

Jake Stevens-Haas
  • 1,186
  • 2
  • 14
  • 26
Ashwini Chaudhary
  • 244,495
  • 58
  • 464
  • 504
  • 1
    If I read that right, `def myfunc(cls: A): ...; myfunc(B())` is OK, (we want to work on instances), `B()` is an instance of [a subclass of] A, not the class [or subclass of] A. OR `def myfunc(cls: Type[A]): ...; myfunc(B)` is OK, (we want to work on classes) `B` is a class (or subclass) A, not an instance [or subclass instance] of A. – ThorSummoner Dec 06 '20 at 20:23
  • 83
    How would that look if an instance of any subclass of A was expected? – Robert McPythons Feb 02 '21 at 14:59
  • The referenced link suggests defining a `TypeVar`, is that necessary? – Tobias Feil Apr 30 '21 at 09:28
  • The answer above did not work -- potentially work anymore. As mentioned above the link in the answers suggests to use `U = TypeVar('U', bound=A)`. This works. – Coolkau Nov 10 '22 at 12:37
26

If we look at the Type description from the typing module, then we see these docs:

A special construct usable to annotate class objects.

For example, suppose we have the following classes::

 class User: ...  # Abstract base for User classes
 class BasicUser(User): ...
 class ProUser(User): ...
 class TeamUser(User): ...

And a function that takes a class argument that's a subclass of User and returns an instance of the corresponding class::

 U = TypeVar('U', bound=User)
 def new_user(user_class: Type[U]) -> U:
     user = user_class()
     # (Here we could write the user object to a database)
     return user

 joe = new_user(BasicUser)

At this point the type checker knows that joe has type BasicUser.

Based on this, I can imagine a synthetic example that reproduces the problem with type hinting errors in PyCharm.

from typing import Type, Tuple

class BaseClass: ...

class SubClass(BaseClass): ...
class SubSubClass(SubClass): ...

def process(model_instance: BaseClass, model_class: Type[BaseClass]) -> Tuple[BaseClass, BaseClass]:
    """ Accepts all of the above classes """
    return model_instance, model_class()


class ProcessorA:
    @staticmethod
    def proc() -> Tuple[SubClass, SubClass]:
        """ PyCharm will show an error 
        `Expected type 'tuple[SubClass, SubClass]', got 'tuple[BaseClass, BaseClass]' instead` """
        return process(SubClass(), SubClass)


class ProcessorB:
    @staticmethod
    def proc() -> Tuple[SubSubClass, SubSubClass]:
        """ PyCharm will show an error 
        `Expected type 'tuple[SubSubClass, SubSubClass]', got 'tuple[BaseClass, BaseClass]' instead` """
        return process(SubSubClass(), SubSubClass)

But we see in docs for Type that the situation can be corrected by using TypeVar with the bound argument. Then use it in places where BaseClass is declared as a type.

from typing import TypeVar, Type, Tuple

class BaseClass: ...

B = TypeVar('B', bound=BaseClass)

class SubClass(BaseClass): ...
class SubSubClass(SubClass): ...

def process(model_instance: B, model_class: Type[B]) -> Tuple[B, B]:
    """ Accepts all of the above classes """
    return model_instance, model_class()


class ProcessorA:
    @staticmethod
    def proc() -> Tuple[SubClass, SubClass]:
        return process(SubClass(), SubClass)


class ProcessorB:
    @staticmethod
    def proc() -> Tuple[SubSubClass, SubSubClass]:
        return process(SubSubClass(), SubSubClass)

Hope this will be helpful.

FRiMN
  • 389
  • 3
  • 7
  • One thing I don’t follow is when do you use `typing.Type` and when do you use `typing.TypeVar`? – rjurney Aug 30 '22 at 18:12
  • 3
    @rjurney you use Type[Foo] when you need to indicate that type is class Foo itself (and not the instance of the class Foo). You use TypeVar when you need to indicate that the type in one place must be the same as the type in the other (i. e. return type must follow argument type, or one argument must be instance of class which is the other argument) – Pavel Shishmarev Sep 08 '22 at 09:47
7

Type[A] accepts also the class itself, which is not always needed.

If you want your function to accept only subclasses, you should go with NewType, like

class A:
    pass

B = NewType('B', A)

def foo(cls: Type[B]):
   ...
Tobias Feil
  • 2,399
  • 3
  • 25
  • 41
BouygudSt
  • 71
  • 2
  • 2
  • 1
    Your answer could be improved with additional supporting information. Please [edit] to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community Aug 02 '22 at 16:29