| name | python-type-hints |
| description | Complete Python type hints system. PROACTIVELY activate for: (1) Built-in generics (list[str], dict[str, int]), (2) Union types (str | None), (3) Type parameter syntax 3.12+, (4) Protocol for structural typing, (5) TypedDict for dict schemas, (6) Literal and Final types, (7) TypeGuard and TypeIs, (8) ParamSpec for decorators, (9) Mypy/Pyright configuration. Provides: Type syntax, Protocol patterns, TypedDict, mypy config. Ensures static type safety with gradual typing strategy.
|
| auto_load | {"enabled":true,"triggers":{"keywords":["type hint","typing","Protocol","TypedDict","mypy","pyright","TypeVar","generic"],"file_patterns":["*.py"]},"priority":"medium","load_with":["python-fundamentals"]} |
Quick Reference
| Type | Syntax (3.9+) | Example |
|---|
| List | list[str] | names: list[str] = [] |
| Dict | dict[str, int] | ages: dict[str, int] = {} |
| Optional | str | None | name: str | None = None |
| Union | int | str | value: int | str |
| Callable | Callable[[int], str] | func: Callable[[int], str] |
| Feature | Version | Syntax |
|---|
| Type params | 3.12+ | def first[T](items: list[T]) -> T: |
| type alias | 3.12+ | type Point = tuple[float, float] |
| Self | 3.11+ | def copy(self) -> Self: |
| TypeIs | 3.13+ | def is_str(x) -> TypeIs[str]: |
| Construct | Use Case |
|---|
Protocol | Structural subtyping (duck typing) |
TypedDict | Dict with specific keys |
Literal["a", "b"] | Specific values only |
Final[str] | Cannot be reassigned |
When to Use This Skill
Use for static type checking:
- Adding type hints to functions and classes
- Creating typed dictionaries with TypedDict
- Defining protocols for duck typing
- Configuring mypy or pyright
- Writing generic functions and classes
Related skills:
- For Python fundamentals: see
python-fundamentals-313
- For testing: see
python-testing
- For FastAPI schemas: see
python-fastapi
Python Type Hints Complete Guide
Overview
Type hints enable static type checking, better IDE support, and self-documenting code. Python's typing system is gradual - you can add types incrementally.
Modern Type Hints (Python 3.9+)
Built-in Generic Types
def process_items(items: list[str]) -> dict[str, int]:
return {item: len(item) for item in items}
names: list[str] = ["Alice", "Bob"]
ages: dict[str, int] = {"Alice": 30, "Bob": 25}
coordinates: tuple[float, float] = (1.0, 2.0)
unique_ids: set[int] = {1, 2, 3}
frozen_data: frozenset[str] = frozenset(["a", "b"])
matrix: list[list[int]] = [[1, 2], [3, 4]]
config: dict[str, list[str]] = {"servers": ["a", "b"]}
Union Types (Python 3.10+)
from typing import Union, Optional
def old_style(value: Union[int, str]) -> Optional[str]:
return str(value) if value else None
def new_style(value: int | str) -> str | None:
return str(value) if value else None
Type Aliases
UserId = int
Username = str
def get_user(user_id: UserId) -> Username:
return "user_" + str(user_id)
from typing import TypeAlias
JsonValue: TypeAlias = str | int | float | bool | None | list["JsonValue"] | dict[str, "JsonValue"]
type Point = tuple[float, float]
type Vector[T] = list[T]
type JsonDict = dict[str, "JsonValue"]
Type Parameters (Python 3.12+)
from typing import TypeVar
T = TypeVar("T")
def first_old(items: list[T]) -> T:
return items[0]
def first[T](items: list[T]) -> T:
return items[0]
class Stack[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()
def merge[K, V](d1: dict[K, V], d2: dict[K, V]) -> dict[K, V]:
return {**d1, **d2}
from typing import SupportsLessThan
def minimum[T: SupportsLessThan](a: T, b: T) -> T:
return a if a < b else b
class Container[T = int]:
def __init__(self, value: T) -> None:
self.value = value
Function Signatures
Basic Functions
from typing import Callable, Iterable, Iterator
def greet(name: str) -> str:
return f"Hello, {name}!"
def create_user(name: str, age: int, email: str | None = None) -> dict:
return {"name": name, "age": age, "email": email}
def log(*args: str, **kwargs: int) -> None:
for arg in args:
print(arg)
for key, value in kwargs.items():
print(f"{key}={value}")
def apply_func(func: Callable[[int, int], int], a: int, b: int) -> int:
return func(a, b)
def make_multiplier(n: int) -> Callable[[int], int]:
def multiplier(x: int) -> int:
return x * n
return multiplier
Overloads
from typing import overload, Literal
@overload
def process(data: str) -> str: ...
@overload
def process(data: bytes) -> bytes: ...
@overload
def process(data: int) -> int: ...
def process(data: str | bytes | int) -> str | bytes | int:
if isinstance(data, str):
return data.upper()
elif isinstance(data, bytes):
return data.upper()
else:
return data * 2
@overload
def fetch(url: str, format: Literal["json"]) -> dict: ...
@overload
def fetch(url: str, format: Literal["text"]) -> str: ...
@overload
def fetch(url: str, format: Literal["bytes"]) -> bytes: ...
def fetch(url: str, format: str) -> dict | str | bytes:
...
ParamSpec for Decorators
from typing import ParamSpec, TypeVar, Callable
from functools import wraps
P = ParamSpec("P")
R = TypeVar("R")
def log_calls(func: Callable[P, R]) -> Callable[P, R]:
@wraps(func)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
print(f"Calling {func.__name__}")
return func(*args, **kwargs)
return wrapper
@log_calls
def add(a: int, b: int) -> int:
return a + b
def log_calls_new[**P, R](func: Callable[P, R]) -> Callable[P, R]:
@wraps(func)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
print(f"Calling {func.__name__}")
return func(*args, **kwargs)
return wrapper
Classes and Protocols
Class Typing
from typing import ClassVar, Self
class User:
count: ClassVar[int] = 0
def __init__(self, name: str, age: int) -> None:
self.name = name
self.age = age
User.count += 1
def with_name(self, name: str) -> Self:
self.name = name
return self
def with_age(self, age: int) -> Self:
self.age = age
return self
user = User("Alice", 30).with_name("Bob").with_age(25)
Protocols (Structural Subtyping)
from typing import Protocol, runtime_checkable
class Drawable(Protocol):
def draw(self) -> None: ...
class Resizable(Protocol):
def resize(self, width: int, height: int) -> None: ...
class DrawableAndResizable(Drawable, Resizable, Protocol):
pass
class Circle:
def draw(self) -> None:
print("Drawing circle")
class Rectangle:
def draw(self) -> None:
print("Drawing rectangle")
def resize(self, width: int, height: int) -> None:
print(f"Resizing to {width}x{height}")
def render(shape: Drawable) -> None:
shape.draw()
render(Circle())
@runtime_checkable
class Closeable(Protocol):
def close(self) -> None: ...
if isinstance(file, Closeable):
file.close()
TypedDict
from typing import TypedDict, Required, NotRequired
class Movie(TypedDict):
title: str
year: int
director: str
movie: Movie = {"title": "Inception", "year": 2010, "director": "Nolan"}
class UserProfile(TypedDict, total=False):
name: str
email: str
age: int
class Article(TypedDict):
title: Required[str]
content: Required[str]
author: NotRequired[str]
tags: NotRequired[list[str]]
class DetailedMovie(Movie):
rating: float
genres: list[str]
Abstract Base Classes
from abc import ABC, abstractmethod
class Repository[T](ABC):
@abstractmethod
def get(self, id: int) -> T | None:
...
@abstractmethod
def save(self, entity: T) -> T:
...
@abstractmethod
def delete(self, id: int) -> bool:
...
class UserRepository(Repository["User"]):
def get(self, id: int) -> "User | None":
return self._db.get(id)
def save(self, entity: "User") -> "User":
return self._db.save(entity)
def delete(self, id: int) -> bool:
return self._db.delete(id)
Advanced Types
Literal Types
from typing import Literal
Mode = Literal["r", "w", "a", "rb", "wb"]
def open_file(path: str, mode: Mode) -> None:
...
open_file("test.txt", "r")
open_file("test.txt", "x")
HttpMethod = Literal["GET", "POST", "PUT", "DELETE", "PATCH"]
StatusCode = Literal[200, 201, 400, 401, 403, 404, 500]
Final and Const
from typing import Final
MAX_SIZE: Final = 100
API_URL: Final[str] = "https://api.example.com"
class Base:
from typing import final
@final
def critical_method(self) -> None:
"""Cannot be overridden in subclasses."""
...
from typing import final
@final
class Singleton:
"""Cannot be subclassed."""
_instance: "Singleton | None" = None
Type Guards
from typing import TypeGuard, TypeIs
def is_string_list(val: list[object]) -> TypeGuard[list[str]]:
return all(isinstance(x, str) for x in val)
def process(items: list[object]) -> None:
if is_string_list(items):
for item in items:
print(item.upper())
def is_int(val: int | str) -> TypeIs[int]:
return isinstance(val, int)
def handle(value: int | str) -> None:
if is_int(value):
print(value + 1)
else:
print(value.upper())
Annotated
from typing import Annotated
from dataclasses import dataclass
UserId = Annotated[int, "Unique user identifier"]
Email = Annotated[str, "Valid email address"]
Age = Annotated[int, "Must be >= 0"]
@dataclass
class User:
id: UserId
email: Email
age: Age
from pydantic import BaseModel, Field
class UserModel(BaseModel):
id: Annotated[int, Field(gt=0)]
email: Annotated[str, Field(pattern=r"^[\w.-]+@[\w.-]+\.\w+$")]
age: Annotated[int, Field(ge=0, le=150)]
Type Checking Tools
Mypy Configuration
[tool.mypy]
python_version = "3.12"
strict = true
warn_return_any = true
warn_unused_ignores = true
disallow_untyped_defs = true
disallow_incomplete_defs = true
check_untyped_defs = true
disallow_untyped_decorators = true
no_implicit_optional = true
warn_redundant_casts = true
warn_unused_configs = true
[[tool.mypy.overrides]]
module = "tests.*"
disallow_untyped_defs = false
[[tool.mypy.overrides]]
module = "third_party.*"
ignore_missing_imports = true
Pyright Configuration
{
"include": ["src"],
"exclude": ["**/node_modules", "**/__pycache__"],
"typeCheckingMode": "strict",
"pythonVersion": "3.12",
"reportMissingImports": true,
"reportMissingTypeStubs": false,
"reportUnusedImport": true,
"reportUnusedVariable": true
}
Running Type Checkers
mypy src/ --strict
mypy src/ --ignore-missing-imports
pyright src/
uv run mypy src/
Best Practices
1. Use Native Generics (3.9+)
items: list[str] = []
mapping: dict[str, int] = {}
from typing import List, Dict
items: List[str] = []
2. Prefer Protocols Over ABCs
from typing import Protocol
class Serializable(Protocol):
def to_json(self) -> str: ...
from abc import ABC, abstractmethod
class SerializableABC(ABC):
@abstractmethod
def to_json(self) -> str: ...
3. Use Abstract Collection Types
from collections.abc import Iterable, Sequence, Mapping, MutableMapping
def process_items(items: Iterable[str]) -> list[str]:
return [item.upper() for item in items]
def lookup(data: Mapping[str, int], key: str) -> int | None:
return data.get(key)
process_items(["a", "b"])
process_items({"a", "b"})
process_items(("a", "b"))
process_items(x for x in "ab")
4. Gradual Typing Strategy
def public_function(data: dict[str, Any]) -> list[str]:
return _internal_helper(data)
def _internal_helper(data):
...
5. Document Complex Types
from typing import TypeAlias
JsonPrimitive: TypeAlias = str | int | float | bool | None
JsonArray: TypeAlias = list["JsonValue"]
JsonObject: TypeAlias = dict[str, "JsonValue"]
JsonValue: TypeAlias = JsonPrimitive | JsonArray | JsonObject
def parse_json(text: str) -> JsonValue:
"""Parse JSON string into typed Python value."""
import json
return json.loads(text)
Additional References
For advanced typing patterns beyond this guide, see:
- Advanced Typing Patterns - Generic repository pattern, discriminated unions, builder pattern with Self, ParamSpec decorators, conditional types with overloads, typed decorator factories, Protocols with class methods, typed context variables, recursive types, typed event systems