python-type-hints

Type Syntax (3.9+) Example

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "python-type-hints" with this command: npx skills add amrahman90/python-expert-agent/amrahman90-python-expert-agent-python-type-hints

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

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"]}

Union Types (Python 3.10+)

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

Type Aliases

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"]

Type Parameters (Python 3.12+)

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

Function Signatures

Basic Functions

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

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

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

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

Classes and Protocols

Class Typing

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)

Protocols (Structural Subtyping)

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()

TypedDict

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]

Abstract Base Classes

from abc import ABC, abstractmethod

class RepositoryT: @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

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]

Final and Const

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

Type Guards

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())

Annotated

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)]

Type Checking Tools

Mypy Configuration

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

Pyright Configuration

// pyrightconfig.json { "include": ["src"], "exclude": ["/node_modules", "/pycache"], "typeCheckingMode": "strict", "pythonVersion": "3.12", "reportMissingImports": true, "reportMissingTypeStubs": false, "reportUnusedImport": true, "reportUnusedVariable": true }

Running Type Checkers

Mypy

mypy src/ --strict mypy src/ --ignore-missing-imports

Pyright (faster, VS Code default)

pyright src/

With uv

uv run mypy src/

Best Practices

  1. Use Native Generics (3.9+)

Preferred (Python 3.9+)

items: list[str] = [] mapping: dict[str, int] = {}

Avoid (old style)

from typing import List, Dict items: List[str] = [] # Deprecated

  1. Prefer Protocols Over ABCs

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: ...

  1. Use Abstract Collection Types

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

  1. Gradual Typing Strategy

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

  1. Document Complex Types

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)

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

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

python-asyncio

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

python-backend

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

python-fundamentals

No summary provided by upstream source.

Repository SourceNeeds Review