Type Hinting for PrincetonPy
Disclaimer: This document is intended to serve as the base for a discussion on Python type hinting I led for the Princeton University Python User Group (i.e. PrincetonPy) on May 1st, 2025. I am not an expert in type hinting and this document is not a true introduction to type hinting in Python. For a more in depth introduction to to type hinting I recommend the static type checking section of the Scientific Python Development Guide or the typing section of SE for Sci.
What is Type Hinting in Python?
Type hinting in Python is the current supported way of adding static strong(ish) typing to Python. It only operates statically, i.e. not at run time, with the help of tools like mypy, pyright, pytype, pyre, and whatever Astral comes up with when they finish their type checker. I’ve only used mypy so I will discuss typing in that context. Mypy, and the other type checkers to various degrees, support gradual typing, only enforcing typing on typed code and ignoring untyped code. You can force it to do typing on all code with the --strict
flag, or more granularly with other flags.
Currently you have, broadly speaking, three options on dealing with types in Python and function/class boundaries when you don’t know what types your users might pass in:
- Lean into duck typing hope for the best, accepting that it might introduce bugs or just crash if the user passes the wrong type
- Put
is instance(var, type)
everywhere - Try to account for every reasonable type the user might pass in, i.e. they might pass in a
str
instead of apathlib.Path
None of these options are particularly good and type hinting hopes to solve, or at least minimize, them.
Language Support
Python 2 is not well supported since it’s been sunsetted for 5 years. The type hinting syntax I’m discussing here was introduced in Python 3.5 and has been expanded and improved upon in most releases since then. I’m currently using Python 3.13 and will assume you are too. I recommend Python 3.10 at a minimum since it significantly improves the union syntax. Python 3.7+ can get Python 3.13 typing with from __future__ import annotations
.
Syntax
Anything in <>
is a placeholder.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# Functions
def f1(x: int,
y: int | float,
z: int = 12) -> int:
return x**2 + y**2 + z**2
# Variables, usually these types can be inferred by the type checker
var: float = 1.2
# Collections
collection: list[int | float] = [1, 2.2]
tup : tuple[int, float, str] = (1, 2.2, "hello")
tup2 : tuple[int, ...] = (1, 2, 3)
d: dict[str, int] = {"one": 1, "two": 2}
d2: dict[str, int | float] = {"one": 1, "two": 2.2}
# Iterables
def f2(it: Iterable[str]): # other generics like `Iterable` exist
for element in it:
do something
# Support any type
from typing import Any
var: Any = <whatever you want>
# Typed Dict (example from https://peps.python.org/pep-0589/)
from typing import TypedDict
# Syntax option 1, total is optional
class Movie(TypedDict, total=<bool>):
# Key type of the value
name: str
year: int
# Syntax option 2, total is optional
Movie = TypedDict('Movie', {'name': str, 'year': int}, total=<bool>)
# Usage
movie: Movie = {'name': 'Blade Runner',
'year': 1982}
Type Narrowing
Type narrowing tracks if an initial declaration was a union (like int | None
) and reduces it to a known, specific, type. Especially useful for optional parameters.
1
2
3
4
5
6
7
8
def f(x: str | None = None):
if x is None:
return ""
return x
# Other ways to type narrow
assert x is not None
assert isinstance(x, str)
Stub Files
Stub files (.pyi
) can be used to add typing to untyped code. Could be used to add typing a third party library that doesn’t support it.
Final
Keyword
The Final
keyword is equivalent to const
in C++. When a variables is declared as Final
it cannot be changed, note that the static type checker enforces this, not the runtime, so that in practice it can be changed but mypy will warn you about that when you run mypy.
1
2
x: Final = 3 # infering the type
x: Final[int] = 3 # explicitely stating the type
Checking Types
1
2
3
4
5
6
# Don't use the built in type() function. Instead use:
import typing
x = <some function> # Type is inferred but you don't know what it is
print(typing.reveal_type(x))
Literals
1
2
3
4
5
6
7
from typing import Literal
# "type" definition for something that can only have a few specific values
def run(action: Literal["start", "stop"]) -> bool:
if action == "start":
return True
return False
Protocols
1
2
3
4
5
6
7
8
9
# Enforce that a more generic type has a specific method. Basically static
#duck typing rather than runtime
from typing import Protocol
class DoesSomething(Protocol):
def do_something(self) -> None: ...
def f(x: DoesSomething) -> None:
x.do_something()
There’s a lot more you can do with protocols, see here for a discussion.
Third Party Libraries
Support for types in third partly libraries is all over the place. I’ll just highlight a few libraries that I’ve used.
- The Python Standard Library: Support is good, generally it just works
- Numpy: Has typing support. Typing numpy objects is incredibly confusing but seems to generally work.
- h5py: No typing support at all
- matplotlib: Seems to work, I haven’t written much typed matplotlib code