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 type support 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.