Today, I came across a function type hinted with type
.
I have done some research as to when one should type hint with type
or Type
, and I can't find a satisfactory answer. From my research it seems there's some overlap between the two.
My question:
type
and Type
?type
vs Type
?Research
Looking at the source for Type
(from typing
tag 3.7.4.3
), I can see this:
# Internal type variable used for Type[]. CT_co = TypeVar('CT_co', covariant=True, bound=type) # This is not a real generic class. Don't use outside annotations. class Type(Generic[CT_co], extra=type): """A special construct usable to annotate class objects. ```
It looks like Type
may just be an alias for type
, except it supports Generic
parameterization. Is this correct?
Example
Here is some sample code made using Python==3.8.5
and mypy==0.782
:
from typing import Type
def foo(val: type) -> None:
reveal_type(val) # mypy output: Revealed type is 'builtins.type'
def bar(val: Type) -> None:
reveal_type(val) # mypy output: Revealed type is 'Type[Any]'
class Baz:
pass
foo(type(bool))
foo(Baz)
foo(Baz()) # error: Argument 1 to "foo" has incompatible type "Baz"; expected "type"
bar(type(bool))
bar(Baz)
bar(Baz()) # error: Argument 1 to "bar" has incompatible type "Baz"; expected "Type[Any]"
Clearly mypy
recognizes a difference.
type
is a metaclass. Just like object instances are instances of classes, classes are instances of metaclasses.
Type
is an annotation used to tell a type checker that a class object itself is to be handled at wherever the annotation is used, instead of an instance of that class object.
There's a couple ways they are related.
type
is applied to an argument is Type
. This is in the same way that list
applied to an argument (like list((1, 2))
) has an annotated returned type of List
. Using reveal_type in:reveal_type(type(1))
we are asking what is the inferred type annotation for the return value of type
when it is given 1. The answer is Type
, more specifically Type[Literal[1]]
.
Type
a type-check-time construct, type
is a runtime construct. This has various implications I'll explain later.Moving onto your examples, in:
class Type(Generic[CT_co], extra=type):
...
We are not annotating extra
as type
, we are instead passing the keyword-argument extra
with value type
to the metaclass of Type
. See Class-level Keyword Arguments for more examples of this construct. Note that extra=type
is very different from extra: type
: one is assigning a value at runtime, and one is annotating with a type hint at type-check time.
Now for the interesting part: if mypy
is able to do successful type checking with both, why use one over the other? The answer lies in that Type
, being a type-check time construct, is much more well integrated with the typing ecosystem. Given this example:
from typing import Type, TypeVar
T = TypeVar("T")
def smart(t: Type[T], v: T) -> T:
return v
def naive(t: type, v: T) -> T:
return v
v1: int = smart(int, 1) # Success.
v2: int = smart(str, 1) # Error.
v3: int = naive(int, 1) # Success.
v4: int = naive(str, 1) # Success.
v1
, v3
and v4
type-check successfully. You can see that v4
from naive
was a false positive, given that the type of 1
is int
, not str
. But because you cannot parametrized the type
metaclass (it is not Generic
), we're unable to get the safety that we have with smart
.
I consider this to be more of a language limitation. You can see PEP 585 which is attempting to bridge the same kind of gap, but for list
/ List
. At the end of the day though, the idea is still the same: the lowercase version is the runtime class, the uppercase version is the type annotation. Both can overlap, but there are features exclusive to both.