Typing for Python Developers
A 5‑Minute Tour with Pyrefly.
Goal: In five minutes you'll know how Python's static type system infers, defines, and composes types—and you'll have copy‑paste snippets to start using right away.
If you are new to Python, check out our Python Typing for New Developers guide.
Python's type system allows you to annotate variables so you, your teammates and your type checker can find bugs before you run your code. Think of it as documentation that's automatically validated and will help your IDE help you.
TL;DR
- Catch bugs before running the code.
- Improve editor autocomplete & refactors.
- Turn your code into living documentation.
Types with Inference
Static analyzers can often infer types from your code—no annotations required. Pyrefly takes this a step further.
Where Inference Shines ✨
- Constant assignments (
answer = 42 -> int
) - List/tuple/dict literals with uniform elements (
names = ["A", "B"] -> list[str]
) - Return types if parameter types are annotated:
When to Add Hints
- Public APIs (library or service boundaries)
- Mixed collections (
list[int | str]
) - Callable signatures (decorators, callbacks)
Define Types Inline
The Basics
Python's built-in types can be used to write many type hints.
Functions
Defining the parameter and return types for a function doesn't just help prevent bugs, but it makes it easier to navigate in other files. You don't always need to define a return type - we'll do our best to infer it for you! We can't always get it right and an explicit return type will help your IDE navigate faster and more accurately.
Advanced Types
Composing Types
The real power comes from composing smaller pieces into richer shapes.
Unions & Optional
Generics
Generics allow you to define reusable functions and classes that work with multiple types. This feature enables you to write more flexible and adaptable code.
Declaring Generic Classes:
Declaring Type Statements:
ParamSpec and TypeVarTuple:
Variance Inference in Generics
When working with generics, a key question is: if one type is a subtype of another, does the subtyping relationship carry over to generic types?
For example, if int
is a subtype of float
, is A[int]
also a subtype of A[float]
?
This behavior is governed by variance:
- Covariant types preserve the direction of subtyping (
A[int]
is a subtype ofA[float]
). - Contravariant types reverse it.
- Invariant types require an exact match.
Before PEP 695 variance had to be declared manually — a process that was often verbose and error-prone. PEP 695 introduced automatic variance inference as part of the new generic class syntax. We fully support this behavior: our type checker analyzes how each type parameter is used — in method arguments, return values, attributes, and base classes — and infers its correct variance automatically.
Example 1: Covariance from Immutable Attributes (Final
)
How Variance is Inferred:
- The attribute
x
is annotated asFinal[T]
, making it immutable after initialization. - Because
T
appears only in this read-only position, it is safe to inferT
as covariant. - This means:
- You can assign
ShouldBeCovariant[int]
to a variable expectingShouldBeCovariant[float]
(sinceint
is a subtype offloat
). - But the reverse is not allowed (
ShouldBeCovariant[float]
toShouldBeCovariant[int]
), which triggers a type error.
- You can assign
Example 2: General Variance Inference from Method and Base Class Usage
How Variance is Inferred:
T1
appears in the base classlist[T1]
. Since list is mutable,T1
is invariant.T2
is used as the type of a method parameter (a: T2
) soT2
contravariant.T3
is the return type of a method (def method2() -> T3
) soT3
is covariant.- This means:
v1
fails due to mismatchedT1
(invariant).v2
fails becauseT2
expects a supertype, but gets a subtype.v4
fails becauseT3
expects a subtype, but gets a supertype.
Protocols
Protocols enable structural typing, which allows you to define interfaces without explicit inheritance. This feature helps you write more modular and composable code.
Structural Types
Python also employs a structural type system, often referred to as "duck typing." This concept is based on the idea that if two objects have the same shape or attributes, they can be treated as being of the same type.
Dataclasses
Dataclasses allow you to create type-safe data structures while minimizing boilerplate.
TypedDict
Typed dictionaries enable you to define dictionaries with specific key-value types. This feature lets you bring type safety to ad-hoc dictionary structures without major refactoring.
Overloads
Overloads allow you to define multiple function signatures for a single function. Like generics, this feature helps you write more flexible and adaptable code.
Typing Features and PEPS available in each Python Version
See the full list of features available in the Python type system here.
Key Highlights Summary:
- Inference: Python's static analyzers can infer types from your code, reducing the need for explicit annotations. This feature enhances code readability and helps catch bugs early.
- Defining Types: You can define types inline using Python's built-in types, which aids in documentation and improves IDE support.
- Advanced Types: The guide covers advanced concepts like composing types, using unions and optionals, generics, protocols, and structural types like dataclasses and TypedDict.
- Practical Examples: The guide includes examples of functions, generic classes, structural typing with protocols, and more, demonstrating how to apply these concepts in real-world scenarios.