Type hints
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.
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))
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:
1lamp_count: int = 122town: str = "Pixel Town"3oil_left: float = 3.54is_night: bool = True5print(lamp_count, town, oil_left, is_night)
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:
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)
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).
What does this print?
1def half(n: int) -> int:2 return n / 23 4print(half(5))
/ always gives a float, so half returns 2.5. The -> int hint promises an int, but Python doesn't check it. A checker such as mypy would point out the broken promise.
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.
| Hint | Means | Example 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"} |
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"]))
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".
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"))
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:
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))
mill: now bridge: at 21
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)
find_lamp returns None, and None + 1 is a TypeError. The | None in the hint is a warning to whoever calls the function: check for None first. mypy would flag this line before the program ever runs.
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".
1def lamps_needed(houses: int, per_house: int = 2) -> int:2 return houses * per_house3 4print(lamps_needed.__annotations__)5print(lamps_needed.__annotations__["return"])
{'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:
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__)
{}
{'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.
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__)
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:
1def lamps_needed(houses: int, per_house: int = 2) -> int:2 return houses * per_house3 4print(lamps_needed("five"))
$ 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.
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.
{'width': <class 'int'>, 'height': <class 'int'>, 'return': <class 'int'>}1def area(width, height):2 return width * height3 4print(area.__annotations__)
Where is the ore?
✓Finish find_cart: return the position of ore in carts, or None if it isn't there.
1 None
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"))
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.
{'amount': <class 'int'>, 'return': None}1class Lamp:2 def __init__(self) -> None:3 self.oil = 04 5 def refill(self, amount):6 self.oil += amount7 8print(Lamp.refill.__annotations__)
What you learned
name: typehints a parameter or a variable;-> typehints 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(orOptional[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.