from pydantic import BaseModel
class User(BaseModel):
id: int
name: str
email: str
# these are type-checked
user = User(id=1, name="Sarah Connor", email="sarah@hotmail.com")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:
x = 1
x = "hello" # not an errorThis is convenient, but also a common source of bugs, since it can be difficult to keep track of what type a variable is.
x = f() # f used to return an int, but now returns a string
x / 2 # so now this raises an errorStatic 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 {
x + 1
}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
typingmodule. - 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 + 1Two 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
-> intsyntax.
It is also possible to annotate individual variables, particularly helpful when the type might not otherwise be clear.
x: int = f()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}
f(3.0)Running mypy on the above code will give you output like:
$ mypy test.py
test.py:3: 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"
Found 2 errors in 1 file (checked 1 source file)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:
intfloatstrboolNonelistdictsettuple- 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 ofintsdict[str, int]- a dictionary withstrkeys andintvaluestuple[int, str]- a two element tuple with anintand astrset[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]- anintorNonetyping.Union[int, str]- anintor astrtyping.Callable[[int, str], bool]- a function that takes anintand astrand 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 xThis means we can use | None as a shorter syntax for Optional:
def f(x: int | None) -> int | None:
return xRuntime 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 = User(id=1, name="Sarah Connor", email=None)
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.