Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 21 additions & 70 deletions docs/guides/typing.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,15 @@
## Basics

The most exciting thing happening right now in Python development is static
typing. Since Python 3.0, we've had function annotations, and since 3.6,
variable annotations. In 3.5, we got a "typing" library, which provides tools to
describe types. This is what static type hints look like:
typing. This is what static type hints look like:

```python
def f(x: int) -> int:
return x * 5
```

This does nothing at runtime, except store the object. If you add
`from __future__ import annotations`, it doesn't even store the actual object,
just the string you type here, so then anything that can pass the Python parser
is allowed here.
This does nothing at runtime, except store the annotation. In Python 3.14+,
annotations are evaluated lazily, only when something asks for them.

It is not useless though! For one, it helps the reader. Knowing the types
expected really gives you a much better idea of what is going on and what you
Expand All @@ -35,34 +31,30 @@ your types.

### Adding types

There are three ways to add types.
There are two ways to add types:

1. They can be inline as annotations. Best for Python 3 code, usually.
2. They can be in special "type comments". Originally designed for Python 2
code, and still requires the proper imports.
3. They can be in a separate file with the same name but with a `.pyi`
1. They can be inline as annotations. This is usually best.
2. They can be in a separate file with the same name but with a `.pyi`
extension. This is important for type stubs or for cases where you don't want
to add imports or touch the original code. You can annotate compiled files or
libraries you don't control this way.

If you have a library you don't control, you can add "type stubs" for it, then
give the type checker your stubs directory. It will pull the types from your
stubs. If you are writing code for a Raspberry Pi, for example, you could add
the stubs for
the Pi libraries, and then validate your code, without ever even installing the
Pi-only libraries!
the stubs for the Pi libraries, and then validate your code, without ever even
installing the Pi-only libraries!

You do not have to add types for every object - most of the time, you just need
it for parameters and returns from functions. When running the type checker, you
can use `reveal_type(...)` to show the inferred type of any object, which is
like a print statement but at type-checking time, or `reveal_locals()` to see
all local types.
like a print statement but at type-checking time. Import it from `typing` if the
code also runs.

### Configuration

By default, the type checker does as little as possible, so that you can add it
iteratively
to a code base. By default:
iteratively to a code base. By default:

- All untyped variables and return values will be `Any`.
- Code inside untyped functions is not checked _at all_.
Expand Down Expand Up @@ -92,7 +84,7 @@ monitors the types of a variable, and "narrows" it when something restricts it.
For example:

```python
x: Union[A, B]
x: A | B
if isinstance(x, A):
reveal_type(x)
else:
Expand All @@ -107,7 +99,7 @@ the code, just type checking it, so both sides of the if are checked.
You can manually force type narrowing with assert:

```python
x: Union[A, B]
x: A | B
assert isinstance(x, A)
reveal_type(x)
```
Expand Down Expand Up @@ -171,52 +163,17 @@ special methods, like `Iterable`, `Iterator`, etc.

Static typing has some great features worth checking out:

- Unions (New syntax in Python 3.10)
- Generic Types (New syntax in Python 3.9)
- Unions (`A | B`)
- Generic Types (like `list[A]`)
- Literals
- TypedDict
- Nicer NamedTuple definition (very popular in Python 3 code)
- Nicer NamedTuple definition
- The type checker validates with the Python version you ask for, regardless of
what version you are actually running.

## Complete example

### Runtime compatible types

Here's the classic syntax, which you need to use if you want to access the type
annotations at runtime and you need to support Python < 3.10:

```python
from typing import Union, List


# Generic types take bracket arguments
def f(x: int) -> List[int]:
return list(range(x))


# Unions are a list of types that all could be allowed
def g(x: Union[str, int]) -> None:
# Type narrowing - Unions get narrowed
if isinstance(x, str):
print("string", x.lower())
else:
print("int", x)

# Calling x.lower() is invalid here!
```

### Types as strings

If you don't access the types at runtime, or if you use Python 3.10+ only, then
you can use a much nicer syntax. The `annotations` future feature causes the
annotations to be stored as strings and not evaluated, which allows you to write
things that are not yet valid, like `list[int]`!

```python
from __future__ import annotations


def f(x: int) -> list[int]:
return list(range(x))

Expand All @@ -228,13 +185,8 @@ def g(x: str | int) -> None:
print("int", x)
```

Notice that there are no imports from typing! Note that you cannot use the "new"
syntax in non annotation locations (like unions in `isinstance`) unless Python
supports it at runtime. And some libraries, like Typer and cattrs, use the
annotations at runtime.

You can use the above in earlier Python versions if you use strings manually,
with the same caveats.
Notice that there are no imports from typing; you can sometimes avoid using it
entirely for simple cases.

## Tips for good types

Expand All @@ -246,7 +198,6 @@ When you have a function, you should take as generic a type as possible, and
return as specific a type as possible. For example:

```python
from __future__ import annotations
from collections.abc import Iterable, Mapping


Expand Down Expand Up @@ -285,12 +236,12 @@ through, usually using TypeVar (or the new generic syntax in Python 3.12, but
that's not available for older Pythons via an import).

Also note that the best place to get these in modern Python is
`collections.abc`, but if you need to subscript them at runtime, you'll need
Python 3.9+ or the versions in `typing`.
`collections.abc`; the ones in `typing` are deprecated if they have a working
alternative in `collections.abc`.

## Final words

When run alongside a good linter like flake8, this can catch a huge number of
When run alongside a good linter like Ruff, this can catch a huge number of
issues before tests or they are discovered in the wild! It also prompts _better
design_, because you are thinking about how types work and interact. It's also
more readable, since if I give you code like this:
Expand Down
Loading