Type Hints & Annotations
Python is dynamically typed — variables don't have a fixed type, and nothing checks types while your program runs. Type hints add optional, explicit type information to function signatures and variables, primarily to help humans and tools understand code better, not to change how Python executes it.
Why Type Hints Exist
Documentation: a signature likedef total(prices: list[float]) -> float:tells readers exactly what to pass in and expect back, without digging through the function body.Static analysis: tools likemypyand IDEs (VS Code, PyCharm) can catch type mismatches before you run the code, similar to a compiler in statically typed languages.Editor support: autocompletion, inline documentation, and refactoring tools all work better when types are known.
def add(a: int, b: int) -> int:
return a + b
print(add(2, 3)) # 5 — used as intended
print(add("2", "3")) # "23" — Python happily runs this, no error, no enforcement!5 23
Basic Syntax
Annotate a parameter with name: type, and annotate the return type with -> type after the closing parenthesis.
def greet(name: str, times: int = 1) -> str:
return (f"Hello, {name}! " * times).strip()
print(greet("Ada"))
print(greet("Ada", times=3))Hello, Ada! Hello, Ada! Hello, Ada! Hello, Ada!
Variables can be annotated too, even without an initial value:
age: int = 30 name: str name = "Ada"
The `typing` Module Basics
Before Python 3.9, container types needed help from the typing module to express things like "a list of strings" or "this value might be None".
from typing import List, Dict, Optional, Union
def get_names() -> List[str]:
return ["Ada", "Grace"]
def get_scores() -> Dict[str, int]:
return {"Ada": 95, "Grace": 98}
def find_user(user_id: int) -> Optional[str]:
users = {1: "Ada"}
return users.get(user_id) # returns a str, or None if missing
def parse_id(value: Union[int, str]) -> int:
return int(value)Typing construct | Meaning |
|---|---|
| A list containing strings |
| A dict mapping strings to ints |
| Either a |
| Either an |
Modern Syntax (Python 3.9 / 3.10+)
Newer Python versions let you use built-in container types directly as generics, and the | operator instead of Union/Optional — no typing import required for these cases.
# Python 3.9+: built-in generics, no typing.List/Dict needed
def get_names() -> list[str]:
return ["Ada", "Grace"]
def get_scores() -> dict[str, int]:
return {"Ada": 95, "Grace": 98}
# Python 3.10+: the '|' operator replaces Union and Optional
def find_user(user_id: int) -> str | None:
users = {1: "Ada"}
return users.get(user_id)
def parse_id(value: int | str) -> int:
return int(value)Older syntax ( | Modern syntax (3.9 / 3.10+) |
|---|---|
|
|
|
|
|
|
|
|
Checking Types Statically with `mypy`
Since Python itself ignores type hints at runtime, a separate tool is needed to actually check them. mypy is the most widely used static type checker — it reads your annotations and reports mismatches without ever running your code.
# calculator.py
def add(a: int, b: int) -> int:
return a + b
result = add("2", "3") # mypy will flag this as an errorpip install mypy mypy calculator.py
calculator.py:5: error: Argument 1 to "add" has incompatible type "str"; expected "int" Found 1 error in 1 file (checked 1 source file)
Quick Reference
Concept | Example |
|---|---|
Parameter annotation |
|
Return annotation |
|
Variable annotation |
|
Optional value |
|
Multiple possible types |
|
List of a type |
|
def find_first_negative(numbers: list[int], start: int = 0) -> int | None:
"""Return the first negative number at or after 'start', or None if there isn't one."""
for n in numbers[start:]:
if n < 0:
return n
return None
print(find_first_negative([3, 7, -2, 9]))
print(find_first_negative([3, 7, 9]))-2 None