typy

beta

Types

Let's look at what fits into a type hint, and how we might describe values more complex than 42.

Data Types

Before runtime, Python classes are just types, so we can use them in our type hints:

from datetime import time

z: None  = None
a: int   = 42
b: float = 4.2
c: str   = "42"
d: bytes = b"42"
e: time  = time(0, 0, 0, 0)

Unions

We can create a new type by combining existing types with a union. Subtypes of the union are subtypes of at least one of the union's members.

a: str | int = 42
a = "hello"

Our variable can have a value of type str or int because both are subtypes of the union type str | int.1

You may also see typing.Optional[x] which is equivalent to a union with None: x | None.

Union's theoretical sibling Intersection, whose subtypes are subtypes of all the intersection's members, does not yet exist in Python.2

Collections

Collections can also be typed. Collection types are generic types and accept type arguments which can be used to specify the type of their members: 3

f: list[str | int] = ["hello", "world", 4, 2]
g: set[int]  = {1, 2, 3}

# The number of arguments is equal to the size of the tuple
h: tuple[int, str, bool] = (0, "xyz", True)

# The first argument describes the keys, the second the values
i: dict[str, str] = {"key": "value"}

Classes

Our own classes can be used as types just like the above. Their subclasses are also their subtypes.

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

class Point3D(Point):
    def __init__(self, x, y, z):
        super().__init__(x, y)
        self.z = z

origin: Point = Point(0, 0)
origin = Point3D(0, 0, 0)

Any

There is also a special Any type. All values are assignable to Any:

a: Any
a = 42
a = 4.2
a = "42"

And Any is compatible with every type:

a: Any = "hello"
b: int = a
c: float = a

Untyped values become Any when their types cannot be inferred. You will most commonly see Any originate in untyped code or libraries. Pyright aliases Any as Unknown when types cannot be determined from imports or library code.4

Any can function as an escape hatch, at the expense of type safety and language tools. It's best to avoid it if possible. When you encounter an Any value, it's worthwhile to see if you can type it.

Literals

The types above all specify the type of a value, but Literal types go further — they specify specific values.5

For example, the variable below has only 5 possible string values:

from typing import Literal

method: Literal["GET", "PUT", "POST", "PATCH", "DELETE"]
method = "GET"
method = "GOT"
#> Expression of type "Literal['GOT']" cannot be assigned to declared type "Literal['GET', 'PUT', 'POST', 'PATCH', 'DELETE']"

Passing multiple values like Literal[1, 2] is a shorthand for a union: Literal[1] | Literal[2].

Literal can be used for any value you can write 'literally,' i.e. without a constructor:

a: Literal[1, 2, 3]
b: Literal["arthur", "zaphod"]
c: Literal[b"1001"]
d: Literal[True]

But the values must be written literally:

a: Literal[42] = int(42)
#> Expression of type "int" cannot be assigned to declared type "Literal[42]"

Inference

You will see slightly different inferred types for some of the above code if you test it. For example:

a: int = 42
b: str = "hello"

The type checker attempts to infer the most specific subtype for the value. For a that is not int, it is Literal[42]. For b, it is Literal["hello"].

Footnotes

  1. Prior to Python 3.10, unions are written with the Union type from the typing module: Union[str, None]

  2. An intersection specification is being drafted for a future PEP

  3. Collection types were imported from the typing module prior to Python 3.9, e.g. from typing import List.

  4. https://microsoft.github.io/pyright/#/type-inference?id=unknown-type

  5. https://docs.python.org/3.10/library/typing.html#typing.Literal https://adamj.eu/tech/2021/07/09/python-type-hints-how-to-use-typing-literal/