Search code examples
pythonpython-typing

Python type hint of type "any type"?


I want to provide a type hint that is any type.

For example the type can be int, float, str, or even a yet-to-be-created custom type. It is being used in a case where I am created custom types, and I'll be creating more.

So I need something like:

def myfunc(entry: Union[str, float, char, MyCustomType1, MyCustomType2, ...]) -> Bool:

where entry can be anything as long as it is a type. To be clear, I am not saying it can be anything. E.g. entry can be int, but it cannot be 7.

It is not clear to me what I can provide in the type hint to represent a Union of any type, including types not yet defined in the module (to allow for the library to grow, and break nothing).


Solution

  • TL;DR

    The solution is to use type[Any] as the annotation. (Docs)


    Explanation

    Why your union is incorrect

    Given the types T1 and T2, when you annotate a variable x with T1 | T2 (or Union[T1, T2] in the old notation), you are saying that x can be either an instance of T1 or an instance of T2. So x can never be the type/class T1 itself or T2 itself.See footnote for exception

    Example: (Playground)

    x: str | float
    x = "spam"  # valid
    x = 3.14    # valid
    x = str     # invalid
    x = float   # invalid
    

    Why type is correct

    Since everything (including types) in Python is an object, everything is an instance of a class. The type of a class is its metaclass. And the base metaclass in Python is type. Therefore the type of any type is... type.

    You can verify this with the built-in isinstance function for any given type.

    Example:

    assert isinstance(str, type)
    assert isinstance(int, type)
    
    class Foo:
        ...
    
    assert isinstance(Foo, type)
    

    This invariant is equivalent to the assertion that everything is an instance of the base class object.

    Example:

    assert isinstance("spam", object)
    assert isinstance(3.14, object)
    
    class Foo:
        ...
    
    assert isinstance(Foo(), object)
    

    So if you want a function f to accept either the type T1 or the type T2 as an argument, you need to write:

    def f(x: type[T1 | T2]) -> object:
        ...
    

    Equivalent:

    def f(x: type[T1] | type[T2]) -> object:
        ...
    

    But if you want it to accept any instance of type (i.e. including any yet-to-be-created class), you need to use type[Any]

    Example: (Playground)

    from typing import Any
    
    def f(x: type[Any]) -> object:
        ...
    
    class Foo:
        ...
    
    f(str)     # valid
    f(float)   # valid
    f(Foo)     # valid
    f("spam")  # invalid
    f(3.14)    # invalid
    f(Foo())   # invalid
    

    Footnote

    Ironically, there are two exceptions to the statement "x: T means you cannot assign the type T itself to x". These two exceptions are object and type.

    type is an instance of type

    To start with the latter, type is of course itself a class. And because the base metaclass in Python is type, this means that type is an instance of itself or in other words type is its own type:

    assert isinstance(type, type)
    

    object is an instance of object

    For object, it may get a bit confusing, but bear with me.

    Statement 1: Because the type hierarchy in Python defines object as the ultimate base class, this means even type is a subclass of object. So everything that is an instance of type is also an instance of object.

    Statement 2: But object is also a type (obviously) and thus an instance of the type (meta-)class.

    Together these two statements imply that object is its own type.

    assert issubclass(type, object)    # `object` is the parent class of `type`
    assert isinstance(object, type)    # `object` has the metaclass `type`
    assert isinstance(object, object)  # `object` has the metaclass `object`
    

    So annotating a variable with object also allows you to assign object itself to it. In fact, there is nothing you cannot assign to it, when it is annotated with object. (Syntax rules notwithstanding of course.)

    Feel free to try: (Playground)

    x: object
    x = object()  # valid
    x = object    # valid
    x = ...       # valid