Search code examples
pythongenericstype-hintingpython-typing

Difference between Python type hints of type and Type?


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:

  • What is the difference between type and Type?
  • What is an example use case that shows when to use 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.


Solution

  • 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.

    1. The annotated return type when 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]].

    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.