107

What is the proper way to annotate a function argument that expects a class object instead of an instance of that class?

In the example below, some_class argument is expected to be a type instance (which is a class), but the problem here is that type is too broad:

def construct(some_class: type, related_data:Dict[str, Any]) -> Any:
    ...

In the case where some_class expects a specific set of types objects, using type does not help at all. The typing module might be in need of a Class generic that does this:

def construct(some_class: Class[Union[Foo, Bar, Baz]], related_data:Dict[str, Any]) -> Union[Foo, Bar, Baz]:
    ...

In the example above, some_class is the Foo, Bar or Faz class, not an instance of it. It should not matter their positions in the class tree because some_class: Class[Foo] should also be a valid case. Therefore,

# classes are callable, so it is OK
inst = some_class(**related_data)

or

# instances does not have __name__
clsname = some_class.__name__

or

# an operation that only Foo, Bar and Baz can perform.
some_class.a_common_classmethod()

should be OK to mypy, pytype, PyCharm, etc.

How can this be done with current implementation (Python 3.6 or earlier)?

Alex Waygood
  • 6,304
  • 3
  • 24
  • 46
Gomes J. A.
  • 1,235
  • 2
  • 9
  • 9
  • 1
    If you need to be more specific than `type`, either introduce a metaclass or an abstract base class. – jonrsharpe Jan 01 '17 at 18:21
  • @jonrsharpe - A metaclass would do the trick, i believe, but I haven't reached this level in Python yet. With the introduction of variable annotations in 3.6 (including a `ClassVar` to differ instance variables from class variables), I wonder why should I use `type` to annotate class objects when there are so many ways to annotate class instances. Maybe I'll have to wait for a future update or a recipe :). – Gomes J. A. Jan 01 '17 at 18:41
  • It seems I'll have to rely on `typing.Type` and do something like `Foo = TypeVar['Foo', bond=Bar]`, where `Bar` is an ABC, then, taking the example above: `def construct(some_class: Type[Foo], ...) -> Foo`. I particularly don't like having to use `TypeVar`, but it seems to be the only way... – Gomes J. A. Jan 03 '17 at 22:06

1 Answers1

136

To annotate an object that is a class, use typing.Type. For example, this would tell the type checker that some_class is class Foo or any of its subclasses:

from typing import Type
class Foo: ...
class Bar(Foo): ...
class Baz: ...
some_class: Type[Foo]
some_class = Foo # ok
some_class = Bar # ok
some_class = Baz # error
some_class = Foo() # error

Note that Type[Union[Foo, Bar, Baz]] and Union[Type[Foo], Type[Bar], Type[Baz]] are completely equivalent.

If some_class could be any of a number of classes, you may want to make them all inherit from the same base class, and use Type[BaseClass]. Note that the inheritance must be non-virtual for now (mypy support for virtual inheritance is being discussed).

Edited to confirm that Type[Union[... is allowed.

max
  • 49,282
  • 56
  • 208
  • 355
  • 2
    Is it safer to use `Union[Type[Class], Type[ClassB]]` or `Type[Union[ClassA, ClassB]]`? I would default to the first choice. – Asclepius Nov 17 '21 at 23:02
  • 10
    Starting with Python 3.9 `typing.Type` is deprecated since you can now use `type[MyClass]` (though mypy has problems with it while it is fine for Pylance). With Python 3.10 the Union expression with `|` was introduced. So now it is possible to write `type[ClassA] | type[ClassB]` or `type[ClassA | ClassB]`. – Pascal Rosin Oct 01 '22 at 22:38