Learn Python / Type Hints

Type Hints

Python never makes you say what type a variable holds. That keeps code short, but in bigger programs it can be hard to tell what a function expects: is price a number or a string? Is items a list or a dictionary? Type hints let you write that information into the code itself.

Your first type hints

Put a colon and a type after a parameter name, and an arrow -> with a type before the colon that ends the def line:

def greet(name: str, times: int) -> str:
    return ("Hello " + name + "! ") * times

print(greet("Ada", 2))
Output

Read it as: greet takes a name that is a str and times that is an int, and it returns a str.

The code runs exactly the same as without the hints. They're there for people reading the code, and for tools such as code editors, which use them to catch mistakes and suggest completions as you type.

Python doesn't enforce them

This is the most important thing to know about type hints: Python itself ignores them when running your code. Passing the "wrong" type doesn't cause an error on its own:

def double(n: int) -> int:
    return n * 2

print(double(5))
print(double("ha"))   # breaks the hint, but Python runs it anyway
Output

To actually check hints, programmers run a separate tool called a type checker, such as mypy, before running the program. It reads the hints and reports mistakes like double("ha") without running anything. Code editors such as VS Code do this checking as you type.

Hints for variables

You can hint variables too, although it's mostly useful when the value doesn't make the type obvious:

count: int = 0
name: str = "Ada"
price: float = 9.99
is_ready: bool = False

print(count, name, price, is_ready)
Output

Hints for collections

Put the type of the items in square brackets:

Hint Means
list[int] a list of ints
set[str] a set of strings
dict[str, float] a dictionary with string keys and float values
tuple[int, int] a tuple of exactly two ints
tuple[str, ...] a tuple of any number of strings
def average(scores: list[float]) -> float:
    return sum(scores) / len(scores)

def stock_value(stock: dict[str, int], prices: dict[str, float]) -> float:
    return sum(count * prices[item] for item, count in stock.items())

def min_max(numbers: list[int]) -> tuple[int, int]:
    return min(numbers), max(numbers)

print(average([7.5, 9.0, 6.0]))
print(stock_value({"apple": 10, "pear": 4}, {"apple": 0.5, "pear": 0.8}))
print(min_max([4, 9, 1]))
Output

Values that might be missing

Often a function returns a value or None, such as a search that might not find anything. Write that as int | None, read as "an int or None":

def find_age(people: dict[str, int], name: str) -> int | None:
    return people.get(name)

ages = {"Ada": 36, "Grace": 85}
print(find_age(ages, "Ada"))
print(find_age(ages, "Alan"))
Output

The | means "or", and it works for any types: int | float, str | list[str], and so on.

A function that doesn't return anything is hinted -> None:

def log(message: str) -> None:
    print("[log]", message)

log("Started")
Output

Seeing the hints

Python stores a function's hints in a dictionary called __annotations__. Printing it shows exactly what was written:

def greet(name: str, times: int) -> str:
    return ("Hello " + name + "! ") * times

print(greet.__annotations__)
Output

The exercises below use this to check your hints.

Hints with classes

Your own classes work as types too. Hints are also how dataclasses (from the Special Methods lesson) know which fields to create:

from dataclasses import dataclass

@dataclass
class Product:
    name: str
    price: float
    in_stock: bool = True

def cheapest(products: list[Product]) -> Product:
    return min(products, key=lambda p: p.price)

items = [Product("Pen", 1.5), Product("Book", 12.99), Product("Eraser", 0.75)]
print(cheapest(items))
Output
Going deeper: Type aliases and the typing module optional

Long hints can be given a name, called a type alias, to keep code readable:

Scores = dict[str, list[int]]

def best_student(scores: Scores) -> str:
    return max(scores, key=lambda name: sum(scores[name]))

print(best_student({"Mia": [80, 90], "Leo": [95, 99]}))
Output

You'll also see the typing module in older code. Before Python 3.9 and 3.10 added the shorter forms, people wrote List[int], Dict[str, int] and Optional[int]. They mean the same as list[int], dict[str, int] and int | None:

from typing import Optional, List

def first(items: List[str]) -> Optional[str]:
    return items[0] if items else None

print(first(["a", "b"]))
print(first([]))
Output

typing also has Any, for "any type at all", and Callable, for functions.

When to use type hints

  • Function parameters and return values are where hints help most, because they're the "contract" other code relies on.
  • Small scripts and quick experiments often don't need them.
  • Be honest: a hint that doesn't match what the code really does is worse than no hint.

Type hints are optional, so you can add them gradually, starting with the functions you use most.

Exercises

Exercise 1: Add the hints

Add type hints to describe: name is a str, age is an int, and it returns a str. The last line prints the hints so the auto-check can see them.

def describe(name, age):
    return f"{name} is {age} years old"

print(describe("Ada", 36))
print(describe.__annotations__)
Output

Exercise 2: A list of numbers

Add hints so that total takes a list of floats and returns a float.

def total(prices):
    return sum(prices)

print(total([2.5, 4.0, 1.25]))
print(total.__annotations__)
Output

Exercise 3: Maybe a result

find_price returns a price from the dictionary, or None if the item isn't there. Add hints: prices is a dictionary of str keys and float values, item is a str, and the return type is "float or None".

def find_price(prices, item):
    return prices.get(item)

menu = {"tea": 2.5, "cake": 3.75}
print(find_price(menu, "cake"))
print(find_price(menu, "soup"))
print(find_price.__annotations__)
Output

Exercise 4: Fix the wrong hint

The return hint on this function is wrong: it says int, but the function returns text. Fix it.

def full_name(first: str, last: str) -> int:
    return first + " " + last

print(full_name("Ada", "Lovelace"))
print(full_name.__annotations__["return"])
Output

Type hints at a glance

A quick reference for later.

Hint Means Example
int, float, str, bool A single value of that type age: int = 36
list[int] A list of ints scores: list[int] = [90, 85]
dict[str, float] A dictionary with string keys and float values prices: dict[str, float] = {"tea": 2.5}
set[str] A set of strings tags: set[str] = {"python"}
tuple[int, int] A tuple of exactly two ints point: tuple[int, int] = (3, 4)
tuple[str, ...] A tuple of any number of strings names: tuple[str, ...] = ("Ada", "Alan")
int | None An int, or None def find(name: str) -> int | None:
int | str An int or a string def show(value: int | str) -> None:
-> None The function returns nothing def log(msg: str) -> None:
Point (a class name) An object of that class def move(p: Point) -> Point:
Optional[int] Older spelling of int | None (from typing import Optional) def find(name: str) -> Optional[int]:
Any Any type at all; switches checking off (from typing import Any) def debug(value: Any) -> None:
Callable[[int], str] A function that takes an int and returns a string (from collections.abc import Callable) def apply(f: Callable[[int], str]) -> str:

Summary

  • Type hints describe what types a function expects and returns: def f(name: str) -> int:.
  • Python ignores hints when running code. Tools like mypy and code editors check them.
  • Collections take item types in brackets: list[int], dict[str, float], tuple[int, int].
  • X | None means "an X, or None". -> None means the function returns nothing.
  • A function's hints are stored in its __annotations__ dictionary.
  • Hints are optional, so add them where they make code clearer, starting with function signatures.