from pydantic import BaseModel
class User(BaseModel):
id: int
str
name: str
email:
# these are type-checked
= User(id=1, name="Sarah Connor", email="sarah@hotmail.com") user
Appendix T: Typing
Goals
- Introduce Python’s type annotation syntax.
Python is a dynamically typed language, which means that the type of a variable is determined at runtime.
It also means the type can change:
= 1
x = "hello" # not an error x
This is convenient, but also a common source of bugs, since it can be difficult to keep track of what type a variable is.
= f() # f used to return an int, but now returns a string
x / 2 # so now this raises an error x
Static Typing
Many languages require variable definitions and function signatures to include type annotations.
// C
int f(int x) {
return x + 1;
}
// Rust
fn f(x: i32) -> i32 {
+ 1
x }
This is called static typing, because the type is checked at compile time. Writing statically typed code can be more challenging at first, but will have fewer type-related errors since the language can enforce constraints.
Type Annotations
As you write larger programs it becomes more difficult and more important to know what types functions written by others are expected to receive & return.
While converting Python to a statically typed language isn’t possible or desirable, as people began to write larger Python programs the desire for something in-between grew.
This led to type annotations, one of Python’s newer features. Initial support was added in an experimental fashion about ten years ago, adoption began in earnest in the last 4-5.
For a sense of how they’ve evolved:
- Python 3.5 (2015) introduced rudimentary type annotations and the
typing
module. - Python 3.6, 3.7, and 3.8 all contained incremental improvements for the type annotation system.
- Python 3.9 (2020) made it possible to use collection types like
list[str]
. - Python 3.10 (2021) introduced union syntax like
int | float
. - Python 3.11, 3.12, and 3.13 have introduced more advanced features, but the core functionality has stabilized.
Annotations are mostly used for function signatures, the def
statement.
This lets us (and our tools) see at a glance what types are expected, both on inputs and outputs.
def f(x: int, y: str) -> int:
print(y)
return x + 1
Two new pieces of syntax:
- After a variable definition (typically a function parameter) you can add a colon and the type. (
: int
) - Return type annotations can be placed after the closing parenthesis of the function signature with the
-> int
syntax.
It is also possible to annotate individual variables, particularly helpful when the type might not otherwise be clear.
int = f() x:
Enforcing Annotations
These annotations are also called type hints because they are not enforced. Unlike a statically-typed language like Rust, these are mere suggestions, Python will still happily take any value in an annotated function.
Instead, they serve a purpose similar to that of docstrings, meant as a reference.
The advantage they have over docstrings is that they are structured data. Your editor and other tools can evaluate them, checking compliance and warning about potential issues.
For example, mypy is a type checker, it works much like ruff
the linter we introduced in Tools.
If we have some code with type annotations that are violated:
def f(x: int) -> str:
return {"x": x}
3.0) f(
Running mypy
on the above code will give you output like:
$ mypy test.py3: error: Incompatible return value type (got "Dict[str, int]", expected "str")
test.py:5: error: Argument 1 to "f" has incompatible type "float"; expected "list"
test.py:2 errors in 1 file (checked 1 source file) Found
Visual Studio Code: Pylance
In Visual Studio code, you can add automatic type checking via Microsoft’s Pylance plugin.
Writing Annotations
You can annotate with any of the built-in types:
int
float
str
bool
None
list
dict
set
tuple
- etc.
The container types allow for annotating the type of the elements in the container with special syntax:
def f(x: list[int]) -> dict[str, int]:
return {str(i): i for i in x}
list[int]
- a list ofint
sdict[str, int]
- a dictionary withstr
keys andint
valuestuple[int, str]
- a two element tuple with anint
and astr
set[tuple[int, int, int]]
- set of tuples, each with threeint
There are many more helper types in the typing
module, for example:
typing.Any
- any typetyping.Optional[int]
- anint
orNone
typing.Union[int, str]
- anint
or astr
typing.Callable[[int, str], bool]
- a function that takes anint
and astr
and returns abool
You can also union types together with |
(as of Python 3.10):
def f(x: int | str) -> int | str:
""" this function takes integers or strings """
return x
This means we can use | None
as a shorter syntax for Optional
:
def f(x: int | None) -> int | None:
return x
Runtime Type Checking
Some libraries, such as the built in dataclasses
module, pydantic
, FastAPI
, and typer
are starting to use type annotations for runtime type checking.
try:
# note: id will be coerced to string since types are compatible
= User(id=1, name="Sarah Connor", email=None)
user except Exception as e:
print(e)
1 validation error for User
email
Input should be a valid string [type=string_type, input_value=None, input_type=NoneType]
For further information visit https://errors.pydantic.dev/2.10/v/string_type
This allows you to catch errors earlier, and can result in less boilerplate code.
Further Exploration
More and more teams are adding type annotation to their Python code, if you write Python for a job there’s a good chance you’ll be asked to annotate your types. Additionally you will find that being able to read type annotations helps you read documentation for Python libraries, which typically denote the types of arguments using annotations.
For more details, see Python’s typing
documentation.