You've already been writing them — name: str, value: int in the dataclass. What about regular functions?
def add(a: int, b: int) -> int:
return a + b
print(add(3, 4))The : int after each parameter and the -> int after the parens are type hints. They document the function's contract: "give me two ints, get an int back."
Does Python actually check them?
No — and that surprises a lot of people. At runtime, type hints are ignored. add("x", "y") runs and returns "xy" — no error. The hints exist for editors and type checkers (mypy, pyright) — tools that read your code statically and complain about mismatches before you run it.
So why bother if they're not checked at runtime?
Three reasons. (1) The hint documents the function — the next reader doesn't have to run code or read the body to know what types you expect. (2) Editors give better autocomplete and warnings as you type. (3) mypy catches whole categories of bugs before they ship.
Where do hints live? Just on parameters and returns?
Mostly. Also on variables (x: int = 5) and on class attributes (which is exactly what @dataclass reads). And the magic spell — __annotations__ — is a dict on every annotated function/class that maps each name to its hint:
print(add.__annotations__)
# {'a': <class 'int'>, 'b': <class 'int'>, 'return': <class 'int'>}def add(a: int, b: int) -> int:
return a + b| Position | Syntax | Meaning |
|---|---|---|
| Parameter | name: type | this argument should be type |
| Return | -> type: | the function returns type |
| Variable | x: int = 5 | this name should hold int |
| Class attribute | name: str (in class body) | this attribute is str |
x: int = 5
y: float = 3.14
s: str = "hi"
b: bool = True
n: None = NoneFor containers, use the typing module — or the bracketed forms (Python 3.9+):
names: list[str] = ["a", "b"]
ages: dict[str, int] = {"a": 30}
pair: tuple[int, str] = (1, "x")
maybe: int | None = None # "int or None" — Python 3.10+def add(a: int, b: int) -> int:
return a + b
print(add("x", "y")) # "xy" — Python doesn't careNo TypeError. Python ignores hints at runtime. The check happens via:
mypy, pyright) — run before commit, fail CIdef parse(line: str) -> tuple[str, int]: tells the next reader the contract without running anything.None where you expected a value, wrong argument order) before runtime.__annotations__ — the runtime viewdef add(a: int, b: int) -> int:
return a + b
print(add.__annotations__)
# {'a': <class 'int'>, 'b': <class 'int'>, 'return': <class 'int'>}Every annotated function and class stores its hints in this dict. @dataclass reads exactly this to know what fields and types to use.
int on a function that sometimes returns None is worse than no hintYou've already been writing them — name: str, value: int in the dataclass. What about regular functions?
def add(a: int, b: int) -> int:
return a + b
print(add(3, 4))The : int after each parameter and the -> int after the parens are type hints. They document the function's contract: "give me two ints, get an int back."
Does Python actually check them?
No — and that surprises a lot of people. At runtime, type hints are ignored. add("x", "y") runs and returns "xy" — no error. The hints exist for editors and type checkers (mypy, pyright) — tools that read your code statically and complain about mismatches before you run it.
So why bother if they're not checked at runtime?
Three reasons. (1) The hint documents the function — the next reader doesn't have to run code or read the body to know what types you expect. (2) Editors give better autocomplete and warnings as you type. (3) mypy catches whole categories of bugs before they ship.
Where do hints live? Just on parameters and returns?
Mostly. Also on variables (x: int = 5) and on class attributes (which is exactly what @dataclass reads). And the magic spell — __annotations__ — is a dict on every annotated function/class that maps each name to its hint:
print(add.__annotations__)
# {'a': <class 'int'>, 'b': <class 'int'>, 'return': <class 'int'>}def add(a: int, b: int) -> int:
return a + b| Position | Syntax | Meaning |
|---|---|---|
| Parameter | name: type | this argument should be type |
| Return | -> type: | the function returns type |
| Variable | x: int = 5 | this name should hold int |
| Class attribute | name: str (in class body) | this attribute is str |
x: int = 5
y: float = 3.14
s: str = "hi"
b: bool = True
n: None = NoneFor containers, use the typing module — or the bracketed forms (Python 3.9+):
names: list[str] = ["a", "b"]
ages: dict[str, int] = {"a": 30}
pair: tuple[int, str] = (1, "x")
maybe: int | None = None # "int or None" — Python 3.10+def add(a: int, b: int) -> int:
return a + b
print(add("x", "y")) # "xy" — Python doesn't careNo TypeError. Python ignores hints at runtime. The check happens via:
mypy, pyright) — run before commit, fail CIdef parse(line: str) -> tuple[str, int]: tells the next reader the contract without running anything.None where you expected a value, wrong argument order) before runtime.__annotations__ — the runtime viewdef add(a: int, b: int) -> int:
return a + b
print(add.__annotations__)
# {'a': <class 'int'>, 'b': <class 'int'>, 'return': <class 'int'>}Every annotated function and class stores its hints in this dict. @dataclass reads exactly this to know what fields and types to use.
int on a function that sometimes returns None is worse than no hintCreate a free account to get started. Paid plans unlock all tracks.