Chapters
Python114 chapters

Beyond the basicsChapter 89 of 114

Type Hints

Say what types you expect, for readers and for tools.

Annotating

Hints go after a colon for parameters, and after an arrow for the return:

Python
def greet(name: str) -> str:
    return f"Hello, {name}"

print(greet("Ada"))

Output

Hello, Ada

Variables can be annotated too, though it is rarely needed when the value is obvious:

Python
count: int = 0
names: list[str] = ["Ada"]
print(count, names)

Output

0 ['Ada']

Python does not enforce them

This is the part that surprises people. Hints are documentation that tools can read; the interpreter ignores them:

Python
def greet(name: str) -> str:
    return f"Hello, {name}"

print(greet(42))

Output

Hello, 42

No error. To catch that you run a checker such as mypy or pyright, usually in your editor and in CI.

Why bother

  • Your editor can autocomplete and catch typos before you run anything
  • A reader knows what to pass without reading the body
  • A checker finds a whole class of bug without writing a test

The cost is some noise. Most projects annotate function signatures and leave local variables bare.

Collections

Since 3.9 the builtin names work directly, and no import is needed:

Python
def totals(scores: dict[str, int]) -> list[int]:
    return sorted(scores.values())

print(totals({"ada": 9, "grace": 7}))

Output

[7, 9]
HintMeans
list[int]a list of integers
dict[str, int]string keys, integer values
tuple[int, int]exactly two integers
tuple[int, ...]any number of integers
set[str]a set of strings

Maybe missing

X | None says the value may be absent. It is so common it has a name, Optional[X], though the pipe form is now preferred:

Python
def find(names: list[str], target: str) -> int | None:
    for index, name in enumerate(names):
        if name == target:
            return index
    return None

print(find(["ada", "grace"], "grace"))
print(find(["ada"], "alan"))

Output

1
None
Python
def add(item: str, target: list[str] | None = None) -> list[str]:
    if target is None:
        target = []
    target.append(item)
    return target

print(add("a"))

Output

['a']

Several possible types

Python
def double(value: int | float | str) -> int | float | str:
    return value * 2

print(double(5), double("ab"))

Output

10 abab

An alias keeps a long hint readable:

Python
type Number = int | float

def half(value: Number) -> Number:
    return value / 2

print(half(9))

Output

4.5

Hinting your own classes

Python
class Dog:
    def __init__(self, name: str) -> None:
        self.name = name

    def rename(self, name: str) -> "Dog":
        self.name = name
        return self

def loudest(dogs: list[Dog]) -> Dog:
    return max(dogs, key=lambda dog: len(dog.name))

print(loudest([Dog("Rex"), Dog("Bartholomew")]).name)

Output

Bartholomew

__init__ returns nothing, so it is annotated -> None.

Checking at runtime, when you need to

Hints are readable from code, which is how libraries like pydantic validate data automatically:

Python
def greet(name: str) -> str:
    return f"Hello, {name}"

print(greet.__annotations__)

Output

{'name': <class 'str'>, 'return': <class 'str'>}

Adopt them gradually

You do not need to annotate everything at once. Start with the functions other code calls most, where the payoff is largest, and leave the rest until you touch it.

Test yourself

2 questions

What does Python do with type hints at runtime?

Show the answer

Nothing; it ignores them — A checker such as mypy or pyright reads them, usually in your editor and in CI.

How should you hint a parameter whose default is None?

Show the answer

int | None — Writing 'x: int = None' is a contradiction, and a checker will point it out.

Next chapter

Async Basics

Do other work while waiting, without threads.