Type Hints are one of the most important features introduced in Python 3.5 through PEP 484. They let you indicate the expected types of variables, function parameters, and return values, making your code more readable, safer, and easier to maintain.
In this complete guide, you'll learn everything about Python Type Hints: from basic concepts to advanced topics like Generics, Protocol, and TypeVar, with practical examples you can apply immediately in your projects.
What Are Type Hints?
Type Hints are type annotations you add to Python code to indicate what kind of data a variable, parameter, or function return should have. They are optional — Python remains a dynamically typed language — but tools like mypy, Pyright, and Pylance use these annotations for static type checking.
def greet(name: str) -> str:
return f"Hello, {name}!"
print(greet("Maria")) # Works
print(greet(42)) # mypy would flag an error
In the example above, name: str indicates the parameter should be a string, and -> str indicates the function returns a string. The code runs normally, but type checkers would flag the second call as incorrect.
Official source: Python Documentation - typing Module
Why Use Type Hints?
Adopting Type Hints brings several benefits:
- More readable code: other developers immediately understand what types each function expects and returns
- Early bug detection: tools like mypy catch type errors before you even run the code
- Smarter autocompletion: IDEs like VS Code, PyCharm, and Vim provide more accurate suggestions based on types
- Living documentation: types serve as self-updating documentation that never goes stale
- Safe refactoring: when you change a function signature, the type checker points out every location that needs adjustment
Source: Real Python - Type Checking in Python
Basic Type Hints
Simple Types
The most basic types are int, float, str, and bool:
def calculate_area(radius: float) -> float:
return 3.14159 * radius ** 2
def is_adult(age: int) -> bool:
return age >= 18
def get_name() -> str:
return "Python"
def process(logical: bool) -> None:
if logical:
print("True")
Note that None is used as the return type when the function doesn't return anything.
Collection Types
For lists, dictionaries, tuples, and sets, use the typing module or, starting from Python 3.9, the built-in types directly:
from typing import List, Dict, Tuple, Set
Python 3.8 and earlier
names: List[str] = ["Ana", "John", "Maria"]
prices: Dict[str, float] = {"banana": 2.50, "apple": 3.00}
coordinate: Tuple[float, float] = (-23.55, -46.63)
ids: Set[int] = {1, 2, 3}
Python 3.9+
names: list[str] = ["Ana", "John", "Maria"]
prices: dict[str, float] = {"banana": 2.50, "apple": 3.00}
From Python 3.9 onward, you can use list[str] instead of List[str], simplifying imports. This change was proposed in PEP 585.
Optional and Union
Often a variable can be more than one type. For that we have Optional and Union:
from typing import Optional, Union
Optional means it can be X or None
def find_user(id: int) -> Optional[str]:
users = {1: "Ana", 2: "John"}
return users.get(id) # May return str or None
Union means it can be any of the listed types
def format_value(value: Union[int, float, str]) -> str:
if isinstance(value, str):
return value
return f"$ {value:.2f}"
Python 3.10+ accepts pipe syntax
def process_id(item_id: int | str) -> bool:
return bool(item_id)
In Python 3.10, PEP 604 introduced the pipe syntax (int | str), making the code cleaner.
Type Aliases
For complex types that repeat, create aliases:
from typing import List, Tuple
Type alias for coordinates
Coordinate = Tuple[float, float]
Route = List[Coordinate]
def calculate_distance(p1: Coordinate, p2: Coordinate) -> float:
from math import sqrt
return sqrt((p2[0] - p1[0])2 + (p2[1] - p1[1])2)
def plan_route(points: Route) -> float:
total_distance = 0.0
for i in range(len(points) - 1):
total_distance += calculate_distance(points[i], points[i + 1])
return total_distance
Aliases make code more semantic and ease future changes.
Typing Functions
Parameters with Default Values
def create_profile(name: str, age: int = 0, active: bool = True) -> dict:
return {"name": name, "age": age, "active": active}
*args and **kwargs
from typing import Any
def sum_all(*args: int) -> int:
return sum(args)
def log(message: str, **kwargs: Any) -> None:
print(f"{message}: {kwargs}")
Callable
For typing functions received as parameters:
from typing import Callable
def execute_operation(
a: int, b: int, operation: Callable[[int, int], int]
) -> int:
return operation(a, b)
result = execute_operation(10, 5, lambda x, y: x + y)
print(result) # 15
Classes and Type Hints
Methods and Self
class Person:
def __init__(self, name: str, age: int) -> None:
self.name = name
self.age = age
def birthday(self) -> None:
self.age += 1
def introduce(self) -> str:
return f"{self.name} is {self.age} years old"
Class Attributes
class Config:
version: str = "1.0"
timeout: int
debug: bool
def __init__(self, timeout: int, debug: bool = False) -> None:
self.timeout = timeout
self.debug = debug
Advanced Types
Literal
Restricts a value to specific constants:
from typing import Literal
def set_mode(mode: Literal["fast", "slow", "eco"]) -> None:
print(f"Mode {mode} activated")
set_mode("fast") # OK
set_mode("turbo") # Type error!
TypedDict
Defines the structure of dictionaries:
from typing import TypedDict
class User(TypedDict):
name: str
email: str
age: int
def create_user(data: User) -> User:
return data
user: User = {"name": "Ana", "email": "[email protected]", "age": 30}
Final
Indicates a value should not be overwritten:
from typing import Final
MAX_ATTEMPTS: Final[int] = 3
PI: Final[float] = 3.14159
Type checkers would flag an error when trying to modify
MAX_ATTEMPTS = 5 # Error!
Generics and TypeVar
Generics let you create functions and classes that work with multiple types safely:
from typing import TypeVar, Generic, List
T = TypeVar("T") # Generic TypeVar
def first_element(items: List[T]) -> T:
return items[0]
print(first_element([1, 2, 3])) # int
print(first_element(["a", "b"])) # str
TypeVar with Constraints
from typing import TypeVar
Number = TypeVar("Number", int, float)
def sum_values(a: Number, b: Number) -> Number:
return a + b
sum_values(1, 2) # OK
sum_values(1.5, 2.5) # OK
sum_values("a", "b") # Error!
Generic in Classes
from typing import Generic, TypeVar, List
T = TypeVar("T")
class Stack(Generic[T]):
def init(self) -> None:
self._items: List[T] = []
def push(self, item: T) -> None:
self._items.append(item)
def pop(self) -> T:
return self._items.pop()
int_stack = Stack[int]()
int_stack.push(10)
int_stack.push(20)
print(int_stack.pop()) # 20
str_stack = Stack[str]()
str_stack.push("Python")
print(str_stack.pop()) # Python
Protocol (Structural Subtyping)
Protocol, introduced in PEP 544, enables static duck typing:
from typing import Protocol
class Speaker(Protocol):
def speak(self) -> str:
...
class Person:
def speak(self) -> str:
return "Hello!"
class Robot:
def speak(self) -> str:
return "Beep boop"
def greet(entity: Speaker) -> None:
print(entity.speak())
greet(Person()) # Hello!
greet(Robot()) # Beep boop
With Protocol, any class implementing the speak method is accepted without needing to inherit from a base class.
Variable Annotations
You can annotate variables directly too:
from typing import List, Optional
name: str = "Python"
version: float = 3.12
tags: List[str] = ["dynamic", "versatile", "modern"]
address: Optional[str] = None
Type Checking Tools
The main tools for static type checking in Python are:
- mypy: the standard tool, created by Jukka Lehtosalo under Guido van Rossum's guidance. Supports most typing module features.
- Pyright: Microsoft's tool, used by Pylance (VS Code extension). Extremely fast with excellent type support.
- pyre: Facebook (Meta)'s type checker for Python.
Official site: mypy official website
Type Hints with Dataclasses
Dataclasses, introduced in Python 3.7, pair perfectly with Type Hints. Check out our complete guide on Python Data Classes.
from dataclasses import dataclass
from typing import List, Optional
@dataclass
class Product:
name: str
price: float
quantity: int = 0
categories: Optional[List[str]] = None
def total_value(self) -> float:
return self.price * self.quantity
product = Product("Notebook", 4500.00, 2)
print(f"Total: $ {product.total_value():.2f}")
Official docs: Python Data Classes
Type Hints with FastAPI and Pydantic
FastAPI uses Type Hints extensively for automatic data validation and documentation generation. To learn more about APIs, see our guide on FastAPI: Creating RESTful APIs.
from fastapi import FastAPI
from pydantic import BaseModel
from typing import List, Optional
app = FastAPI()
class Item(BaseModel):
name: str
price: float
available: bool = True
tags: Optional[List[str]] = None
@app.post("/items/")
async def create_item(item: Item) -> Item:
return item
@app.get("/items/{item_id}")
async def read_item(item_id: int) -> dict:
return {"item_id": item_id}
Pydantic uses Type Hints to validate and convert data automatically. Learn more at: Pydantic Documentation
Best Practices with Type Hints
1. Be Gradual
You don't need to type everything at once. Start with public functions and main APIs, then expand gradually.
2. Prefer Specific Types
Instead of Any, use more specific types whenever possible. Any disables type checking.
from typing import Any
Avoid
def process(data: Any) -> Any:
return data
Prefer
def process(data: list[str]) -> list[str]:
return data
3. Use Optional Correctly
Optional[X] is equivalent to Union[X, None]. Use it when the value can be None.
4. Configure Your Type Checker
Create a pyproject.toml or mypy.ini file to configure mypy:
# mypy.ini
[mypy]
python_version = 3.12
strict = True
ignore_missing_imports = True
5. Document Complex Cases
For very complex types, add comments or docstrings explaining the logic.
Type Hints in Python 3.12 and 3.13
The latest Python versions bring significant improvements to the type system:
- PEP 695 (Python 3.12): new syntax for TypeVar and Generics, more concise.
- PEP 698 (Python 3.13): improved
typesupport in type hints. - PEP 649 (Python 3.14, upcoming): deferred evaluation of annotations by default, replacing
from __future__ import annotations.
Source: PEP 695 - Type Parameter Syntax
Complete Example: Order System
Let's apply everything we've learned in a real-world example:
from dataclasses import dataclass, field
from typing import List, Optional, Protocol, TypeVar
from datetime import datetime, date
from decimal import Decimal
T = TypeVar("T")
class Repository(Protocol[T]):
def save(self, entity: T) -> None:
...
def find_by_id(self, id_: int) -> Optional[T]:
...
@dataclass
class Customer:
id: int
name: str
email: str
registration_date: date = field(default_factory=date.today)
@dataclass
class OrderItem:
product: str
quantity: int
unit_price: Decimal
@dataclass
class Order:
id: int
customer: Customer
items: List[OrderItem]
created_at: datetime = field(default_factory=datetime.now)
status: str = "pending"
def calculate_total(self) -> Decimal:
return sum(
item.quantity * item.unit_price
for item in self.items
)
def update_status(self, new_status: str) -> None:
self.status = new_status
class CustomerRepository:
def save(self, customer: Customer) -> None:
print(f"Saving customer {customer.name}")
def find_by_id(self, id_: int) -> Optional[Customer]:
return Customer(id=id_, name="Ana", email="[email protected]")
def process_order(
order: Order,
repository: Repository[Order]
) -> bool:
try:
repository.save(order)
order.update_status("processed")
return True
except Exception:
return False
Usage
customer = Customer(id=1, name="Maria", email="[email protected]")
item = OrderItem(product="Notebook", quantity=1, unit_price=Decimal("4500.00"))
order = Order(id=100, customer=customer, items=[item])
repo = CustomerRepository()
print(f"Order total: $ {order.calculate_total():.2f}")
process_order(order, repo) # type: ignore
Conclusion
Type Hints are a powerful tool that makes Python suitable for projects of all sizes. They improve code readability, prevent bugs, simplify refactoring, and enhance the development experience with modern IDEs.
Start adopting types gradually in your projects — you'll see the difference in code quality and maintainability. And remember: Type Hints are optional, but the benefits they bring are enormous!
Keep learning with Universo Python. Explore more content about Data Classes, FastAPI, and other advanced Python topics.