pyright/docs/type-concepts.md
2024-01-21 20:35:11 -08:00

5.5 KiB
Raw Blame History

Static Typing: The Basics

Getting started with static type checking in Python is easy, but its important to understand a few simple concepts. In addition to the documentation below, you may also find the community-maintained Static Typing Documentation to be of use. That site also includes the official Specification for the Python Type System.

Type Declarations

When you add a type annotation to a variable or a parameter in Python, you are declaring that the symbol will be assigned values that are compatible with that type. You can think of type annotations as a powerful way to comment your code. Unlike text-based comments, these comments are readable by both humans and enforceable by type checkers.

If a variable or parameter has no type annotation, Pyright will assume that any value can be assigned to it.

Type Assignability

When your code assigns a value to a symbol (in an assignment expression) or a parameter (in a call expression), the type checker first determines the type of the value being assigned. It then determines whether the target has a declared type. If so, it verifies that the type of the value is assignable to the declared type.

Lets look at a few simple examples. In this first example, the declared type of a is float, and it is assigned a value that is an int. This is permitted because int is assignable to float.

a: float = 3

In this example, the declared type of b is int, and it is assigned a value that is a float. This is flagged as an error because float is not assignable to int.

b: int = 3.4  # Error

This example introduces the notion of a Union type, which specifies that a value can be one of several distinct types. A union type can be expressed using the | operator to combine individual types.

c: int | float = 3.4
c = 5
c = a
c = b
c = None  # Error
c = ""  # Error

This example introduces the Optional type, which is the same as a union with None.

d: Optional[int] = 4
d = b
d = None
d = ""  # Error

Those examples are straightforward. Lets look at one that is less intuitive. In this example, the declared type of f is list[int | None]. A value of type list[int] is being assigned to f. As we saw above, int is assignable to int | None. You might therefore assume that list[int] is assignable to list[int | None], but this is an incorrect assumption. To understand why, we need to understand generic types and type arguments.

e: list[int] = [3, 4]
f: list[int | None] = e  # Error

Generic Types

A generic type is a class that is able to handle different types of inputs. For example, the list class is generic because it is able to operate on different types of elements. The type list by itself does not specify what is contained within the list. Its element type must be specified as a type argument using the indexing (square bracket) syntax in Python. For example, list[int] denotes a list that contains only int elements whereas list[int | float] denotes a list that contains a mixture of int and float elements.

We noted above that list[int] is not assignable to list[int | None]. Why is this the case? Consider the following example.

my_list_1: list[int] = [1, 2, 3]
my_list_2: list[int | None] = my_list_1  # Error
my_list_2.append(None)

for elem in my_list_1:
    print(elem + 1)  # Runtime exception

The code is appending the value None to the list my_list_2, but my_list_2 refers to the same object as my_list_1, which has a declared type of list[int]. The code has violated the type of my_list_1 because it no longer contains only int elements. This broken assumption results in a runtime exception. The type checker detects this broken assumption when the code attempts to assign my_list_1 to my_list_2.

list is an example of a mutable container type. It is mutable in that code is allowed to modify its contents — for example, add or remove items. The type parameters for mutable container types are typically marked as invariant, which means that an exact type match is enforced. This is why the type checker reports an error when attempting to assign a list[int] to a variable of type list[int | None].

Most mutable container types also have immutable counterparts.

Mutable Type Immutable Type
list Sequence
dict Mapping
set Container
n/a tuple

Switching from a mutable container type to a corresponding immutable container type is often an effective way to resolve type errors relating to assignability. Lets modify the example above by changing the type annotation for my_list_2.

my_list_1: list[int] = [1, 2, 3]
my_list_2: Sequence[int | None] = my_list_1  # No longer an error

The type error on the second line has now gone away.

For more details about generic types, type parameters, and invariance, refer to PEP 483 — The Theory of Type Hints.

Debugging Types

When you want to know the type that the type checker has evaluated for an expression, you can use the special reveal_type() function:

x = 1
reveal_type(x)  # Type of "x" is "Literal[1]"

This function is always available and does not need to be imported. When you use Pyright within an IDE, you can also simply hover over an identifier to see its evaluated type.