Kodewick
Lesson 31 of 31Contents

Type hints

Lesson 31 of 31 · Part 6 · Going further · about 20 minutes

Python never asks what type a variable holds. That keeps short programs short, but in a big one it is easy to forget what a function expects: a number, a string, a list of what? Type hints write the answer into the code. They are notes for people, editors and checking tools, and this last lesson of the course shows how to write them and what they do (and don't do).

Your first type hints

A hint goes after a parameter's name, following a colon. The return type goes after ->, before the colon that ends the def line. A default value comes after the hint.

main.py
1def greet(name: str) -> str:2    return f"Good evening, {name}!"3 4def lamps_needed(houses: int, per_house: int = 2) -> int:5    return houses * per_house6 7print(greet("Wick"))8print(lamps_needed(5))
Output
Good evening, Wick!
10

Read the second one aloud: "lamps_needed takes an int houses and an int per_house, which is 2 if you leave it out, and returns an int." Variables can have hints too, with the same colon:

main.py
1lamp_count: int = 122town: str = "Pixel Town"3oil_left: float = 3.54is_night: bool = True5print(lamp_count, town, oil_left, is_night)
Output
12 Pixel Town 3.5 True

Most variables don't need one: in lamp_count = 12 anyone can see it is an int. Variable hints help where it is not obvious, like an empty list or dictionary that gets filled later.

Python doesn't check them

This surprises almost everyone: Python ignores the hints when it runs your code. Pass the wrong type and nothing stops you:

main.py
1def lamps_needed(houses: int, per_house: int = 2) -> int:2    return houses * per_house3 4print(lamps_needed("ab"))5print(lamps_needed(2.5))6count: int = 57count = "five"8print(count)
Output
abab
5.0
five

"ab" * 2 is "abab", so the call simply works, with a result nobody wanted. The hints did not raise an error; they are not rules but promises, and nobody here is checking whether they are kept. That is what tools like mypy are for (see below).

Guess first

What does this print?

1def half(n: int) -> int:2    return n / 23 4print(half(5))

Hints for containers

For a list, say what is in it: list[int] is a list of ints. A dictionary needs two types, one for the keys and one for the values.

HintMeansExample value
list[int]a list of ints[3, 8, 2]
dict[str, int]a dictionary from strings to ints{"mill": 5}
tuple[str, int]a tuple of a str, then an int("Ada", 12)
set[str]a set of strings{"iron", "gold"}
main.py
1def total_ore(loads: list[int]) -> int:2    return sum(loads)3 4def count_words(words: list[str]) -> dict[str, int]:5    counts: dict[str, int] = {}6    for word in words:7        counts[word] = counts.get(word, 0) + 18    return counts9 10print(total_ore([3, 8, 2]))11print(count_words(["iron", "gold", "iron"]))
Output
13
{'iron': 2, 'gold': 1}

Here the variable hint earns its place: counts = {} alone says nothing about what will go in. In older code you will see List[int] and Dict[str, int], imported from the typing module; that was needed before Python 3.9 and means the same thing.

Values that may be missing

Many functions return a value or None, for example a search that may find nothing. Write that as int | None: "an int, or None".

main.py
1def find_lamp(oil: dict[str, int], name: str) -> int | None:2    if name in oil:3        return oil[name]4    return None5 6oil = {"mill": 5, "bridge": 0}7print(find_lamp(oil, "mill"))8print(find_lamp(oil, "gate"))
Output
5
None

The typing module has an older spelling of the same thing, Optional[int], which you will meet in a lot of code. It also fits parameters whose default is None:

main.py
1from typing import Optional2 3def light(name: str, hour: Optional[int] = None) -> str:4    if hour is None:5        return f"{name}: now"6    return f"{name}: at {hour}"7 8print(light("mill"))9print(light("bridge", 21))
Output
mill: now
bridge: at 21
Guess first

What happens?

1def find_lamp(oil: dict[str, int], name: str) -> int | None:2    if name in oil:3        return oil[name]4    return None5 6print(find_lamp({"mill": 5}, "gate") + 1)

Looking at the hints

Python doesn't check the hints, but it does keep them. Every function has a dictionary __annotations__ with one entry per hint; the return type is under "return".

main.py
1def lamps_needed(houses: int, per_house: int = 2) -> int:2    return houses * per_house3 4print(lamps_needed.__annotations__)5print(lamps_needed.__annotations__["return"])
Output
{'houses': <class 'int'>, 'per_house': <class 'int'>, 'return': <class 'int'>}
<class 'int'>

A plain type like int shows as <class 'int'>, the class itself. A container hint is kept as you wrote it, list[str], and so is str | None. A parameter without a hint has no entry, and -> None is stored as None:

main.py
1def area(width, height):2    return width * height3 4def greet(names: list[str], title: str | None = None) -> None:5    print(f"Hi, {names}")6 7print(area.__annotations__)8print(greet.__annotations__)
Output
{}
{'names': list[str], 'title': str | None, 'return': None}

You will rarely read __annotations__ yourself, but tools do: this is how @dataclass finds a class's fields, and how some libraries check data for you.

Hints in classes

Methods get hints like any function. self needs none, and __init__, which returns nothing, gets -> None. In a dataclass (lesson 28) the hints are not optional: they are what makes a line a field.

main.py
1from dataclasses import dataclass2 3class Lamp:4    def __init__(self, name: str, oil: int = 0) -> None:5        self.name = name6        self.oil = oil7 8    def refill(self, amount: int) -> None:9        self.oil += amount10 11@dataclass12class Cart:13    ore: str14    weight: int15 16lamp = Lamp("mill")17lamp.refill(4)18print(lamp.name, lamp.oil)19print(Cart("iron", 12))20print(Lamp.refill.__annotations__)
Output
mill 4
Cart(ore='iron', weight=12)
{'amount': <class 'int'>, 'return': None}

Checking hints, and when to use them

To have the hints checked, run a type checker over your code before you run it. The best known is mypy, installed with pip on your own computer (it doesn't run in the browser). Given this file:

lamps.py
1def lamps_needed(houses: int, per_house: int = 2) -> int:2    return houses * per_house3 4print(lamps_needed("five"))
Terminal
$ pip install mypy
$ mypy lamps.py
lamps.py:4: error: Argument 1 to "lamps_needed" has incompatible type "str"; expected "int"  [arg-type]
Found 1 error in 1 file (checked 1 source file)

mypy finds the mistake without running anything. Editors such as VS Code and PyCharm read the hints too: they underline a wrong type as you type and suggest the right methods after a dot.

When are hints worth writing?

  • Functions other people call, or that you will come back to in a month: the hints are documentation that can't go out of date unnoticed.
  • Bigger programs, with a checker: it finds whole kinds of bugs, like a forgotten None, before they happen.
  • Dataclasses, which need them.
  • Not needed for short scripts and experiments, like most examples in this course. Hints are optional; add them where they help.

Practice

Write the code, then press Check. A hint costs nothing; the answer is there when you need it.

Exercise 1

Hint the area

Add type hints to area: both parameters are ints, and so is the result. The last line will then print its hints.

It should print
{'width': <class 'int'>, 'height': <class 'int'>, 'return': <class 'int'>}
exercise1.py
1def area(width, height):2    return width * height3 4print(area.__annotations__)
Output
Press Run or Ctrl+Enter
Exercise 2

Where is the ore?

Finish find_cart: return the position of ore in carts, or None if it isn't there.

It should print
1
None
exercise2.py
1def find_cart(carts: list[str], ore: str) -> int | None:2    pass3 4print(find_cart(["iron", "gold", "copper"], "gold"))5print(find_cart(["iron", "gold", "copper"], "silver"))
Output
Press Run or Ctrl+Enter
Exercise 3

Hint a method

Add type hints to refill: amount is an int, and the method returns nothing. The last line will then print its hints.

It should print
{'amount': <class 'int'>, 'return': None}
exercise3.py
1class Lamp:2    def __init__(self) -> None:3        self.oil = 04 5    def refill(self, amount):6        self.oil += amount7 8print(Lamp.refill.__annotations__)
Output
Press Run or Ctrl+Enter

What you learned

  • name: type hints a parameter or a variable; -> type hints what a function returns.
  • Python doesn't check hints when it runs: they are promises for people, editors and checkers like mypy.
  • Containers say what they hold: list[int], dict[str, int], tuple[str, int], set[str].
  • int | None (or Optional[int]) marks a value that may be missing.
  • function.__annotations__ holds the hints; dataclasses use them to find their fields.

That was the last lesson. You can now write real programs: take an idea of your own, like a to-do list, a quiz, or a tracker for Wick's lamps, and build it in the Python Playground, a little at a time. When you get stuck, come back to the lesson you need: every example here is still there to run and step through.