Python Type Hints and Dataclasses

Make code clearer and catch bugs earlier with type hints, then write data-holding classes in a few lines with @dataclass.

Lesson 39 of 107Advanced17 min read

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:

Python
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, bool and None.
  • 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:

HintMeaning
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 | strEither an int or a string
str | NoneA 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:

SyntaxNeeds
list[int], dict[str, int]Python 3.9+ (use typing.List, typing.Dict before)
int | str, str | NonePython 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

Python
from dataclasses import dataclass

@dataclass
class Product:
    name: str
    price: float
    stock: int = 0

From those three lines, Python generates:

  • __init__, so you can write Product("Mug", 8.5, stock=12).
  • __repr__, so printing shows Product(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: type and -> return_type; Python doesn't enforce them at run time.
  • Use list[int], dict[str, int], int | str and str | None for richer types.
  • Editors and mypy use hints to warn you about mistakes before the code runs.
  • @dataclass generates __init__, __repr__ and __eq__ from typed fields.
  • Use field(default_factory=list) for list defaults, frozen=True for immutable objects and order=True for sorting.

Python Type Hints Examples

Each example shows the code, the output it produces and what each part does.

Example 1

A typed function that may return None

Python
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}")
Output
cake: £3.75
soup: not on the menu
Line-by-line explanation
  1. Line 1The hints say: a dictionary of names to prices, a string, and the result is a float or None.
  2. Line 7Because the return type includes None, mypy would warn if you used price without checking it first.
Example 2

A dataclass in action

Python
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"]))
Output
Order(customer='Sam', items=['Coffee'], paid=False)
Order(customer='Priya', items=['Tea'], paid=True)
True
Line-by-line explanation
  1. Lines 3 to 7Three typed fields, two with defaults. No __init__ written by hand.
  2. Lines 9 to 10Normal methods work as usual.
  3. Lines 15 to 16The generated __repr__ shows every field.
  4. Line 17The generated __eq__ compares field values.
Example 3

Frozen and ordered dataclasses

Python
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 = 3
Output
Version(major=2, minor=0, patch=0)
Version(major=1, minor=2, patch=3)
FrozenInstanceError: cannot assign to field 'major'
Line-by-line explanation
  1. Line 3order=True compares major, then minor, then patch, just like real version numbers.
  2. Lines 10 to 11Because the fields are numbers, 1.10.0 correctly sorts after 1.2.3.
  3. Line 14frozen=True stops the object from being changed.

Common Python Type Hints Mistakes

Expecting hints to be enforced

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

print(double("ha"))     # prints haha: no error at run time
Do
# 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

Don't
@dataclass
class Team:
    members: list[str] = []      # ValueError: mutable default
Do
@dataclass
class Team:
    members: list[str] = field(default_factory=list)

Defaults before required fields

Don't
@dataclass
class User:
    active: bool = True
    name: str              # TypeError: non-default after default
Do
@dataclass
class User:
    name: str
    active: bool = True

Python 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.

Fill in the blankseasy

Complete the hint so the function is marked as returning a string.

def initials(name: str)  str:
    return name[0]
Multiple choiceeasy

What does the hint list[int] mean?

Predict the outputmedium

What does this print?

from dataclasses import dataclass

@dataclass
class Point:
    x: int
    y: int = 0

print(Point(3))
Multiple choicemedium

Which hint means "a string or nothing"?

Multiple choicehard

Why does @dataclass(frozen=True) let objects be used as dictionary keys?

Python Type Hints Quiz

5 questions to check what you learned.

  1. 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
  2. Which methods does @dataclass generate by default?

    • Only __init__
    • __str__ and __len__
    • All comparison operators
    • __init__, __repr__ and __eq__
  3. 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()
  4. What does -> None on a function mean?

    • It returns zero
    • It can't be called
    • It returns an empty string
    • It doesn't return a value
  5. With @dataclass(order=True) and fields major, 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