Python doesn't make you say what type a variable holds. That makes small scripts quick to write, but in bigger programs it raises questions. Does get_user(id) expect a number or a string? Does it return a user or None when there isn't one? You end up reading the function's code, or finding out the hard way when the program crashes.
Type hints answer those questions right in the code: def get_user(user_id: int) -> User | None:. Python itself doesn't enforce them at run time, but your editor uses them for better autocomplete and warnings, and tools like mypy check a whole project for type mistakes before you run it.
Type hints pair perfectly with dataclasses. A class that mainly holds data usually needs an __init__, a readable __repr__ and an __eq__ for comparison. The @dataclass decorator writes all of that for you from a list of typed fields. In this lesson you'll learn both, and see how they make code safer and shorter.
Annotating variables and functions
Add a colon and a type after a name, and -> before a function's return type:
name: str = "Ada"
age: int = 36
def greet(name: str, excited: bool = False) -> str:
return f"Hello, {name}{'!' if excited else '.'}"- The basic types are
int,float,str,boolandNone. - A function that returns nothing is annotated
-> None. - Hints are notes for people and tools. Python won't stop you calling
greet(42), but your editor and mypy will warn you.
Collections, optional values and unions
Put the item type in square brackets:
| Hint | Meaning |
|---|---|
list[str] | A list of strings |
dict[str, float] | Keys are strings, values are floats |
tuple[int, int] | Exactly two ints |
set[str] | A set of strings |
int | str | Either an int or a string |
str | None | A string, or None if there isn't one |
str | None is the modern way to write Optional[str], which you'll still see in older code imported from the typing module. These forms work in Python 3.10 and later.
Which Python version supports which syntax
Type hint syntax has become shorter over several releases, so the version you target matters:
| Syntax | Needs |
|---|---|
list[int], dict[str, int] | Python 3.9+ (use typing.List, typing.Dict before) |
int | str, str | None | Python 3.10+ (use Union, Optional before) |
type Point = tuple[float, float] | Python 3.12+ type alias statement |
On older versions you can add from __future__ import annotations at the top of a file. It stores hints as text instead of evaluating them, so the newer syntax works inside annotations even on Python 3.7 and 3.8.
Writing a dataclass
from dataclasses import dataclass
@dataclass
class Product:
name: str
price: float
stock: int = 0From those three lines, Python generates:
__init__, so you can writeProduct("Mug", 8.5, stock=12).__repr__, so printing showsProduct(name='Mug', price=8.5, stock=12).__eq__, so two products with the same values are equal.
You can still add your own methods, such as def total_value(self) -> float:, just like a normal class.
Defaults, frozen and ordering
- Fields with defaults must come after fields without them.
- For a default list or dict, use
field(default_factory=list), never= [], for the same reason as the mutable default trap. @dataclass(frozen=True)makes objects immutable: changing a field raises an error, and the objects can be used in sets and as dictionary keys.@dataclass(order=True)adds<and>, comparing fields in order, so a list of objects can be sorted directly.
Python Type Hints Syntax
Type hints and dataclasses
def func(a: int, b: str = "x") -> list[str]: ...
value: dict[str, float] = {}
maybe: str | None = None
from dataclasses import dataclass, field
@dataclass(frozen=True, order=True)
class Name:
field1: type
field2: type = default
items: list[str] = field(default_factory=list)Python Type Hints Summary
- Type hints use
name: typeand-> return_type; Python doesn't enforce them at run time. - Use
list[int],dict[str, int],int | strandstr | Nonefor richer types. - Editors and mypy use hints to warn you about mistakes before the code runs.
@dataclassgenerates__init__,__repr__and__eq__from typed fields.- Use
field(default_factory=list)for list defaults,frozen=Truefor immutable objects andorder=Truefor sorting.
Python Type Hints Examples
Each example shows the code, the output it produces and what each part does.
A typed function that may return None
def find_price(menu: dict[str, float], item: str) -> float | None:
return menu.get(item)
menu = {"tea": 2.5, "cake": 3.75}
for item in ["cake", "soup"]:
price = find_price(menu, item)
if price is None:
print(f"{item}: not on the menu")
else:
print(f"{item}: £{price:.2f}")cake: £3.75
soup: not on the menuLine-by-line explanation
- Line 1The hints say: a dictionary of names to prices, a string, and the result is a float or None.
- Line 7Because the return type includes None, mypy would warn if you used price without checking it first.
A dataclass in action
from dataclasses import dataclass, field
@dataclass
class Order:
customer: str
items: list[str] = field(default_factory=list)
paid: bool = False
def add(self, item: str) -> None:
self.items.append(item)
a = Order("Sam")
a.add("Coffee")
b = Order("Priya", ["Tea"], paid=True)
print(a)
print(b)
print(a == Order("Sam", ["Coffee"]))Order(customer='Sam', items=['Coffee'], paid=False)
Order(customer='Priya', items=['Tea'], paid=True)
TrueLine-by-line explanation
- Lines 3 to 7Three typed fields, two with defaults. No __init__ written by hand.
- Lines 9 to 10Normal methods work as usual.
- Lines 15 to 16The generated __repr__ shows every field.
- Line 17The generated __eq__ compares field values.
Frozen and ordered dataclasses
from dataclasses import dataclass
@dataclass(frozen=True, order=True)
class Version:
major: int
minor: int
patch: int
releases = [Version(1, 10, 0), Version(1, 2, 3), Version(2, 0, 0)]
print(max(releases))
print(sorted(releases)[0])
v = Version(1, 0, 0)
v.major = 3Version(major=2, minor=0, patch=0)
Version(major=1, minor=2, patch=3)
FrozenInstanceError: cannot assign to field 'major'Line-by-line explanation
- Line 3order=True compares major, then minor, then patch, just like real version numbers.
- Lines 10 to 11Because the fields are numbers, 1.10.0 correctly sorts after 1.2.3.
- Line 14frozen=True stops the object from being changed.
Common Python Type Hints Mistakes
Expecting hints to be enforced
def double(n: int) -> int:
return n * 2
print(double("ha")) # prints haha: no error at run time# run a type checker to catch it before running:
# mypy app.py -> error: Argument 1 has incompatible type "str"A list as a dataclass default
@dataclass
class Team:
members: list[str] = [] # ValueError: mutable default@dataclass
class Team:
members: list[str] = field(default_factory=list)Defaults before required fields
@dataclass
class User:
active: bool = True
name: str # TypeError: non-default after default@dataclass
class User:
name: str
active: bool = TruePython Type Hints in Real Projects
Types in real projects
Large Python codebases at companies of every size use type hints and run mypy or a similar checker in their automated tests. Frameworks such as FastAPI and Pydantic go further and use the hints at run time to validate incoming data and generate documentation. Dataclasses are the standard way to model records like orders, settings and API responses without writing boilerplate.
Python Type Hints FAQ
Does Python check type hints when the program runs?
No. The interpreter stores hints but ignores them while running, so passing a string where an int is hinted still runs. Static checkers such as mypy or Pyright, and your editor, read the hints and report mismatches before you run the code. Libraries such as Pydantic can validate data against hints at run time if you need that.
What is the difference between Optional[str] and str | None?
They mean exactly the same thing: a string or None. Optional[str] comes from the typing module and works on older Python versions. str | None is the newer, shorter syntax, available from Python 3.10.
When should I use a dataclass instead of a normal class?
Use a dataclass when the class mainly stores data and you would otherwise write a repetitive __init__, __repr__ and __eq__ by hand. Use a regular class when construction needs complex logic or the class is mainly about behaviour rather than fields.
What is the difference between a dataclass and a dictionary?
A dictionary accepts any keys at any time, so a typo such as user["emial"] silently creates a new entry. A dataclass has a fixed set of named, typed fields, so editors can autocomplete them and a misspelt attribute name raises an error. Dictionaries remain the right choice for data with unpredictable keys, such as raw JSON.
Can I add validation to a dataclass?
Yes. Define a __post_init__(self) method; the generated __init__ calls it after setting the fields. Check values there and raise ValueError for bad data, for example a negative price.
Python Type Hints Exercises
Answer each one and check it instantly. If you get stuck, open the hint before the answer.
Complete the hint so the function is marked as returning a string.
def initials(name: str) str:
return name[0]
What does the hint list[int] mean?
What does this print?
from dataclasses import dataclass
@dataclass
class Point:
x: int
y: int = 0
print(Point(3))
Which hint means "a string or nothing"?
Why does @dataclass(frozen=True) let objects be used as dictionary keys?
Python Type Hints Quiz
5 questions to check what you learned.
Does Python stop the program if an argument doesn't match its type hint?
- No, hints are checked by tools like editors and mypy, not at run time
- Yes, always
- Only for ints
- Only in classes
Which methods does
@dataclassgenerate by default?- Only __init__
- __str__ and __len__
- All comparison operators
- __init__, __repr__ and __eq__
How do you give a dataclass field an empty list as its default?
- items: list = []
- items: list[str] = list
- items: list[str] = field(default_factory=list)
- items: list[str] = None()
What does
-> Noneon a function mean?- It returns zero
- It can't be called
- It returns an empty string
- It doesn't return a value
With
@dataclass(order=True)and fieldsmajor, minor, how are objects compared?- Field by field, starting with major, then minor if major is equal
- By the order they were created
- By memory address
- Alphabetically by class name