website/docs/typing-for-python-developers.mdx
{/*
import CodeSnippet from '@site/src/sandbox/CodeSnippet'
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 typing, check out our Python Typing 101 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
Static analyzers can often infer types from your code—no annotations required. Pyrefly takes this a step further.
<CodeSnippet sampleFilename="basic_inference.py" codeSample={`# Basic Inference from typing import reveal_type
answer = 42 reveal_type(answer) # hover to reveal type
fruits = ["apple", "banana", "cherry"] scores = {"math": 95, "science": 90}
def greet(name): return f"Hello, {name}!"
message = greet("World") `} />
answer = 42 -> int)names = ["A", "B"] -> list[str])<CodeSnippet
sampleFilename="return_inference.py"
codeSample={def add(a: int, b: int): # ✅ param annotations return a + b # 🔍 return inferred -> int }
/>
list[int | str])Python's built-in types can be used to write many type hints. <CodeSnippet sampleFilename="built_in_types.py" codeSample={`# Example: Basic Types
from typing import reveal_type
age: int = 5 reveal_type(age) # revealed type: int
age = "oops"
name: str = "John" reveal_type(name) # revealed type: str
numbers: list[int] = [1, 2, 3] reveal_type(numbers) # revealed type: list[int]
names: list[str] = ["John", "Jane"] reveal_type(names) # revealed type: list[str]
person: dict[str, str] = {"name": "John", "age": "30"} reveal_type(person) # revealed type: dict[str, str]
is_admin = True reveal_type(is_admin) # revealed type: Literal[True] `} />
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. <CodeSnippet sampleFilename="functions_types.py" codeSample={`# Example: Functions
from typing import reveal_type
def greet(name: str) -> str: return f"Hello, {name}!"
greet("Pyrefly")
def whatDoesThisFunctionReturnAgain(a: int, b: int): return a + b
reveal_type(whatDoesThisFunctionReturnAgain(2, 3)) # revealed type: int `} />
The real power comes from composing smaller pieces into richer shapes.
<CodeSnippet sampleFilename="unions_types.py" codeSample={`# Union and Optional Types
from typing import Optional
def to_int(data: str | bytes | None) -> Optional[int]: if data is None: return None if isinstance(data, bytes): data = data.decode() return int(data) `} />
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: <CodeSnippet sampleFilename="generics.py" codeSample={`# Example: Generic Classes
from typing import reveal_type
class C[T]: def init(self, x: T): self.x = x def box(self) -> list[T]: return [self.x]
c = C(0) reveal_type(c.box()) # revealed type: list[int] `} />
Declaring Type Statements:
<CodeSnippet
sampleFilename="type_statements.py"
codeSample={# Example: Type Statements type ListOrSet[T:int] = list[T] | set[T] }
/>
ParamSpec and TypeVarTuple:
<CodeSnippet
sampleFilename="param_spec_typevar_tuple.py"
codeSample={# Example: ParamSpec and TypeVarTuple class ChildClass[T, *Ts, **P]: ... }
/>
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:
A[int] is a subtype of A[float]).Before PEP 695, variance had to be declared manually and was often confusing. Pyrefly infers the variance automatically based on how each type parameter is used - in method arguments, return values, attributes, and base classes.
Example 1: Covariance from Immutable Attributes (Final)
<CodeSnippet sampleFilename="variance1.py" codeSample={`# Example 1: Variance Inference
from typing import Final
class ShouldBeCovariant[T]: x: Final[T]
def __init__(self, value: T):
self.x = value
x1: ShouldBeCovariant[float] = ShouldBeCovariantint # OK x2: ShouldBeCovariant[int] = ShouldBeCovariantfloat # ERROR! `} />
How Variance is Inferred:
x is annotated as Final[T], making it immutable after initialization.T appears only in this read-only position, it is safe to infer T as covariant.ShouldBeCovariant[int] to a variable expecting ShouldBeCovariant[float] (since int is a subtype of float).ShouldBeCovariant[float] to ShouldBeCovariant[int]), which triggers a type error.Example 2: General Variance Inference from Method and Base Class Usage
<CodeSnippet sampleFilename="variance2.py" codeSample={`# Example 2: Variance Inference
class ClassAT1, T2, T3: def method1(self, a: T2) -> None: ...
def method2(self) -> T3:
...
def func_a(p1: ClassA[float, int, int], p2: ClassA[int, float, float]): v1: ClassA[int, int, int] = p1 # ERROR! v2: ClassA[float, float, int] = p1 # ERROR! v3: ClassA[float, int, float] = p1 # OK
v4: ClassA[int, int, int] = p2 # ERROR!
v5: ClassA[int, int, float] = p2 # OK
`} />
How Variance is Inferred:
T1 appears in the base class list[T1]. Since list is mutable, T1 is invariant.T2 is used as the type of a method parameter (a: T2) so T2 contravariant.T3 is the return type of a method (def method2() -> T3) so T3 is covariant.v1 fails due to mismatched T1 (invariant).v2 fails because T2 expects a supertype, but gets a subtype.v4 fails because T3 expects a subtype, but gets a supertype.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 allow you to create type-safe data structures while minimizing boilerplate.
<CodeSnippet sampleFilename="data_classes.py" codeSample={`# Example: Dataclasses
from dataclasses import dataclass
@dataclass class Point: x: float y: float
Point(x=0.0, y=0.0) # OK Point(x=0.0, y="oops") # ERROR! `} />
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.
<CodeSnippet sampleFilename="typed_dict.py" codeSample={`# Example: TypedDict
from typing import TypedDict
class Movie(TypedDict): name: str year: int
good_movie: Movie = {"name": "Toy Story", "year": 1995} # OK bad_movie: Movie = {"name": "The Room", "year": "2003"} # ERROR! `} />
Overloads allow you to define multiple function signatures for a single function. Like generics, this feature helps you write more flexible and adaptable code.
<CodeSnippet sampleFilename="overloads.py" codeSample={`# Example: Overloads
from typing import overload, reveal_type
@overload def f(x: int) -> int: ...
@overload def f(x: str) -> str: ...
def f(x: int | str) -> int | str: return x
reveal_type(f(0)) # revealed type: int reveal_type(f("")) # revealed type: str `} />
Protocols allows you to define interfaces without explicit inheritance. This feature helps you write more modular and composable code.
<CodeSnippet sampleFilename="protocols.py" codeSample={`# Example: Structural Typing with Protocols
from typing import Iterable, Protocol
class Writer(Protocol): def write(self) -> None: ...
class GoodWorld: def write(self) -> None: print("Hello world!")
class BadWorld: pass
def f(writer: Writer): pass
f(GoodWorld()) # OK f(BadWorld()) # ERROR! `} />
See the full list of features available in the Python type system here.