Help us improve
Share bugs, ideas, or general feedback.
From python-master
Provides complete Python type hints: built-in generics, unions, TypedDict, Protocol, Literal, ParamSpec, and mypy/Pyright config for static type safety.
npx claudepluginhub josiahsiegel/claude-plugin-marketplace --plugin python-masterHow this skill is triggered — by the user, by Claude, or both
Slash command
/python-master:python-type-hintsThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
| Type | Syntax (3.9+) | Example |
Guides Python type hints, mypy static checking with configs, modern syntax, and advanced features like Protocol, TypedDict, Generics for type-safe code.
Enforces Python type safety with hints, generics, protocols, type narrowing, and mypy/pyright. Use for annotating code, generic classes, structural interfaces, or strict checking.
Provides Python type hint patterns for variables, functions, collections, TypedDict, Generics, Protocols, unions, and structural typing. Ensures type safety with mypy and pyright.
Share bugs, ideas, or general feedback.
| 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 |
Use for static type checking:
Related skills:
python-fundamentals-313python-testingpython-fastapiType hints enable static type checking, better IDE support, and self-documenting code. Python's typing system is gradual - you can add types incrementally.
# Python 3.9+ - Use built-in types directly
# No need for typing.List, typing.Dict, etc.
def process_items(items: list[str]) -> dict[str, int]:
return {item: len(item) for item in items}
# Collections
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"])
# Nested generics
matrix: list[list[int]] = [[1, 2], [3, 4]]
config: dict[str, list[str]] = {"servers": ["a", "b"]}
# Old way (still works)
from typing import Union, Optional
def old_style(value: Union[int, str]) -> Optional[str]:
return str(value) if value else None
# New way (Python 3.10+)
def new_style(value: int | str) -> str | None:
return str(value) if value else None
# Optional is just Union with None
# Optional[str] == str | None
# Simple type alias
UserId = int
Username = str
def get_user(user_id: UserId) -> Username:
return "user_" + str(user_id)
# Complex type alias
from typing import TypeAlias
JsonValue: TypeAlias = str | int | float | bool | None | list["JsonValue"] | dict[str, "JsonValue"]
# Python 3.12+ type statement
type Point = tuple[float, float]
type Vector[T] = list[T]
type JsonDict = dict[str, "JsonValue"]
# Old way with TypeVar
from typing import TypeVar
T = TypeVar("T")
def first_old(items: list[T]) -> T:
return items[0]
# New way (Python 3.12+)
def first[T](items: list[T]) -> T:
return items[0]
# Generic classes
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()
# Multiple type parameters
def merge[K, V](d1: dict[K, V], d2: dict[K, V]) -> dict[K, V]:
return {**d1, **d2}
# Bounded type parameters
from typing import SupportsLessThan
def minimum[T: SupportsLessThan](a: T, b: T) -> T:
return a if a < b else b
# Default type parameters (Python 3.13+)
class Container[T = int]:
def __init__(self, value: T) -> None:
self.value = value
from typing import Callable, Iterable, Iterator
# Simple function
def greet(name: str) -> str:
return f"Hello, {name}!"
# Multiple parameters
def create_user(name: str, age: int, email: str | None = None) -> dict:
return {"name": name, "age": age, "email": email}
# *args and **kwargs
def log(*args: str, **kwargs: int) -> None:
for arg in args:
print(arg)
for key, value in kwargs.items():
print(f"{key}={value}")
# Callable type
def apply_func(func: Callable[[int, int], int], a: int, b: int) -> int:
return func(a, b)
# Higher-order functions
def make_multiplier(n: int) -> Callable[[int], int]:
def multiplier(x: int) -> int:
return x * n
return multiplier
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 with Literal
@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:
# Implementation
...
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
# Python 3.12+ syntax
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
from typing import ClassVar, Self
class User:
# Class variable
count: ClassVar[int] = 0
def __init__(self, name: str, age: int) -> None:
self.name = name
self.age = age
User.count += 1
# Self type for method chaining
def with_name(self, name: str) -> Self:
self.name = name
return self
def with_age(self, age: int) -> Self:
self.age = age
return self
# Usage
user = User("Alice", 30).with_name("Bob").with_age(25)
from typing import Protocol, runtime_checkable
# Define a protocol (interface)
class Drawable(Protocol):
def draw(self) -> None: ...
class Resizable(Protocol):
def resize(self, width: int, height: int) -> None: ...
# Combining protocols
class DrawableAndResizable(Drawable, Resizable, Protocol):
pass
# Implementation (no explicit inheritance needed!)
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}")
# Works because Circle has draw() method
def render(shape: Drawable) -> None:
shape.draw()
render(Circle()) # OK - Circle satisfies Drawable protocol
# Runtime checkable protocol
@runtime_checkable
class Closeable(Protocol):
def close(self) -> None: ...
# Can use isinstance
if isinstance(file, Closeable):
file.close()
from typing import TypedDict, Required, NotRequired
# Basic TypedDict
class Movie(TypedDict):
title: str
year: int
director: str
movie: Movie = {"title": "Inception", "year": 2010, "director": "Nolan"}
# With optional keys
class UserProfile(TypedDict, total=False):
name: str # Optional
email: str # Optional
age: int # Optional
# Mixed required and optional (Python 3.11+)
class Article(TypedDict):
title: Required[str]
content: Required[str]
author: NotRequired[str]
tags: NotRequired[list[str]]
# Inheritance
class DetailedMovie(Movie):
rating: float
genres: list[str]
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)
from typing import Literal
# Restrict to specific values
Mode = Literal["r", "w", "a", "rb", "wb"]
def open_file(path: str, mode: Mode) -> None:
...
open_file("test.txt", "r") # OK
open_file("test.txt", "x") # Type error!
# Combining literals
HttpMethod = Literal["GET", "POST", "PUT", "DELETE", "PATCH"]
StatusCode = Literal[200, 201, 400, 401, 403, 404, 500]
from typing import Final
# Constant that shouldn't be reassigned
MAX_SIZE: Final = 100
API_URL: Final[str] = "https://api.example.com"
# Final class methods
class Base:
from typing import final
@final
def critical_method(self) -> None:
"""Cannot be overridden in subclasses."""
...
# Final classes
from typing import final
@final
class Singleton:
"""Cannot be subclassed."""
_instance: "Singleton | None" = None
from typing import TypeGuard, TypeIs
# TypeGuard (narrows type in if block)
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):
# items is now list[str]
for item in items:
print(item.upper())
# TypeIs (Python 3.13+ - stricter than TypeGuard)
def is_int(val: int | str) -> TypeIs[int]:
return isinstance(val, int)
def handle(value: int | str) -> None:
if is_int(value):
# value is int
print(value + 1)
else:
# value is str (properly narrowed)
print(value.upper())
from typing import Annotated
from dataclasses import dataclass
# Metadata for validation/documentation
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
# With Pydantic
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)]
# pyproject.toml
[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
# Per-module overrides
[[tool.mypy.overrides]]
module = "tests.*"
disallow_untyped_defs = false
[[tool.mypy.overrides]]
module = "third_party.*"
ignore_missing_imports = true
// pyrightconfig.json
{
"include": ["src"],
"exclude": ["**/node_modules", "**/__pycache__"],
"typeCheckingMode": "strict",
"pythonVersion": "3.12",
"reportMissingImports": true,
"reportMissingTypeStubs": false,
"reportUnusedImport": true,
"reportUnusedVariable": true
}
# Mypy
mypy src/ --strict
mypy src/ --ignore-missing-imports
# Pyright (faster, VS Code default)
pyright src/
# With uv
uv run mypy src/
# Preferred (Python 3.9+)
items: list[str] = []
mapping: dict[str, int] = {}
# Avoid (old style)
from typing import List, Dict
items: List[str] = [] # Deprecated
# Preferred - structural typing
from typing import Protocol
class Serializable(Protocol):
def to_json(self) -> str: ...
# Less flexible - nominal typing
from abc import ABC, abstractmethod
class SerializableABC(ABC):
@abstractmethod
def to_json(self) -> str: ...
from collections.abc import Iterable, Sequence, Mapping, MutableMapping
# Prefer abstract types for function parameters
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)
# Works with any iterable/mapping
process_items(["a", "b"]) # list
process_items({"a", "b"}) # set
process_items(("a", "b")) # tuple
process_items(x for x in "ab") # generator
# Start with public API
def public_function(data: dict[str, Any]) -> list[str]:
return _internal_helper(data)
# Type internal helpers later
def _internal_helper(data): # Untyped initially
...
# Aim for 80%+ coverage on new code
# Use # type: ignore sparingly
from typing import TypeAlias
# Use type aliases for complex types
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)
For advanced typing patterns beyond this guide, see: