python-type-hints-guide

Python Type Hints Guide

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-guide" with this command: npx skills add clostaunau/holiday-card/clostaunau-holiday-card-python-type-hints-guide

Python Type Hints Guide

Purpose

This skill provides comprehensive guidance on Python type hints and static type checking for Python 3.9+ codebases. It serves as a reference during code reviews to ensure proper type annotation usage, evaluate type hint quality, and promote best practices for gradual typing adoption.

This skill should be referenced by the uncle-duke-python agent when:

  • Reviewing Python code for type hint usage

  • Evaluating type annotation quality and consistency

  • Identifying type hint anti-patterns

  • Recommending mypy configuration improvements

  • Guiding gradual typing adoption strategies

Context

Type hints (introduced in PEP 484) enable static type checking in Python while maintaining the language's dynamic nature. Modern Python (3.9+) has significantly improved type hint syntax with built-in generic types and the union operator. Proper type hints improve:

  • Code documentation: Self-documenting function signatures

  • IDE support: Better autocomplete and refactoring

  • Bug detection: Catch type errors before runtime

  • Maintainability: Easier to understand code contracts

  • Refactoring safety: Catch breaking changes early

This guide focuses on modern Python (3.9+) patterns and assumes familiarity with basic Python syntax.

Prerequisites

  • Python 3.9 or later (for built-in generic types)

  • Python 3.10+ recommended (for union operator |)

  • mypy installed for static type checking: pip install mypy

  • Understanding of Python's dynamic typing system

Type Hints Basics (PEP 484)

Basic Types

Use built-in type names directly for simple types:

def greet(name: str) -> str: return f"Hello, {name}!"

def add(x: int, y: int) -> int: return x + y

def calculate_average(scores: list[float]) -> float: return sum(scores) / len(scores)

def is_valid(flag: bool) -> bool: return not flag

None Type

Use None for functions that don't return a value:

def log_message(message: str) -> None: print(f"[LOG] {message}") # No return statement or explicit return None

Optional Types

Use Optional[T] or T | None (Python 3.10+) for values that can be None:

from typing import Optional

Python 3.9 style

def find_user(user_id: int) -> Optional[dict]: # May return dict or None return None

Python 3.10+ style (preferred)

def find_user_modern(user_id: int) -> dict | None: # May return dict or None return None

With default None parameter

def greet(name: str, title: str | None = None) -> str: if title: return f"Hello, {title} {name}!" return f"Hello, {name}!"

Collection Types (Python 3.9+)

Python 3.9+ allows using built-in collection types directly (lowercase):

Modern Python 3.9+ (preferred)

def process_names(names: list[str]) -> dict[str, int]: return {name: len(name) for name in names}

def unique_items(items: set[int]) -> list[int]: return sorted(items)

def get_coordinates() -> tuple[float, float]: return (42.0, -71.0)

Variable-length tuple (homogeneous)

def process_values(values: tuple[int, ...]) -> int: return sum(values)

Nested collections

def process_matrix(matrix: list[list[float]]) -> float: return sum(sum(row) for row in matrix)

Legacy Python 3.8 and below (avoid in modern code):

from typing import List, Dict, Set, Tuple

def process_names(names: List[str]) -> Dict[str, int]: return {name: len(name) for name in names}

Union Types

Use Union[T, U] or T | U (Python 3.10+) for multiple possible types:

from typing import Union

Python 3.9 style

def process_id(user_id: Union[int, str]) -> str: return str(user_id)

Python 3.10+ style (preferred)

def process_id_modern(user_id: int | str) -> str: return str(user_id)

Multiple types

def parse_value(value: int | float | str | None) -> float: if value is None: return 0.0 return float(value)

Any Type

Use Any when type cannot be determined or for gradual typing:

from typing import Any

Accept any type (escape hatch)

def legacy_function(data: Any) -> Any: # Used when migrating untyped code return data

Dict with any values

def process_config(config: dict[str, Any]) -> None: # Config can have values of any type pass

Warning: Overusing Any defeats the purpose of type hints. Use sparingly and document why.

Advanced Types

Generic Types (TypeVar, Generic)

Create reusable generic functions and classes:

from typing import TypeVar, Generic

Type variable for generic functions

T = TypeVar('T')

def first_item(items: list[T]) -> T | None: return items[0] if items else None

Usage preserves type information

names: list[str] = ["Alice", "Bob"] first_name: str | None = first_item(names) # Type checker knows this is str | None

numbers: list[int] = [1, 2, 3] first_number: int | None = first_item(numbers) # Type checker knows this is int | None

Generic class

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 | None:
    return self._items.pop() if self._items else None

Usage

string_stack: Stack[str] = Stack() string_stack.push("hello") # OK string_stack.push(42) # Type error!

Bounded type variable (constrains to subclasses)

from numbers import Number NumT = TypeVar('NumT', bound=Number)

def add_numbers(x: NumT, y: NumT) -> NumT: return x + y # type: ignore[return-value]

Protocol Classes (Structural Subtyping - PEP 544)

Define interfaces based on structure (duck typing with type checking):

from typing import Protocol

class Drawable(Protocol): def draw(self) -> None: ...

class Circle: def draw(self) -> None: print("Drawing circle")

class Square: def draw(self) -> None: print("Drawing square")

Both Circle and Square satisfy Drawable protocol

without explicit inheritance

def render(shape: Drawable) -> None: shape.draw()

render(Circle()) # OK render(Square()) # OK

Protocol with properties

class Sized(Protocol): @property def size(self) -> int: ...

Real-world example: file-like objects

class SupportsRead(Protocol): def read(self, size: int = -1) -> str: ...

def process_file(file: SupportsRead) -> str: return file.read()

Literal Types (PEP 586)

Specify exact literal values allowed:

from typing import Literal

def set_log_level(level: Literal["DEBUG", "INFO", "WARNING", "ERROR"]) -> None: print(f"Log level set to {level}")

set_log_level("DEBUG") # OK set_log_level("TRACE") # Type error!

Multiple literals

def open_file(path: str, mode: Literal["r", "w", "a", "rb", "wb"]) -> None: pass

Literal booleans (rarely needed)

def process(flag: Literal[True]) -> None: # Only accepts True, not False pass

Combining with Union

Mode = Literal["read", "write", "append"] def process_mode(mode: Mode | None = None) -> None: pass

TypedDict (PEP 589)

Define dictionaries with specific key-value type requirements:

from typing import TypedDict, NotRequired

Basic TypedDict

class User(TypedDict): name: str age: int email: str

def create_user(user: User) -> None: print(f"Creating user: {user['name']}")

Usage

user: User = {"name": "Alice", "age": 30, "email": "alice@example.com"} create_user(user) # OK

Missing required key

bad_user: User = {"name": "Bob", "age": 25} # Type error! Missing 'email'

Optional keys (Python 3.11+)

class UserOptional(TypedDict): name: str age: int email: NotRequired[str] # Optional key

For Python 3.9-3.10, use total=False

class PartialUser(TypedDict, total=False): email: str phone: str

class RequiredUser(PartialUser): name: str # Required age: int # Required

Inheritance

class Employee(User): employee_id: int department: str

Final Types (PEP 591)

Indicate values that should not be reassigned or overridden:

from typing import Final, final

Final variable (constant)

MAX_CONNECTIONS: Final[int] = 100

Type error if reassigned

MAX_CONNECTIONS = 200 # Type error!

Final class attribute

class Config: API_URL: Final[str] = "https://api.example.com"

Final method (cannot be overridden)

class Base: @final def process(self) -> None: print("Processing")

class Derived(Base): def process(self) -> None: # Type error! Cannot override @final method print("Custom processing")

Final class (cannot be subclassed)

@final class ImmutablePoint: def init(self, x: float, y: float) -> None: self.x = x self.y = y

class Point3D(ImmutablePoint): # Type error! Cannot subclass @final class pass

NewType

Create distinct types for type safety:

from typing import NewType

Create new types for domain concepts

UserId = NewType('UserId', int) OrderId = NewType('OrderId', int)

def get_user(user_id: UserId) -> dict: return {"id": user_id, "name": "User"}

def get_order(order_id: OrderId) -> dict: return {"id": order_id, "status": "pending"}

Usage

user_id = UserId(42) order_id = OrderId(100)

get_user(user_id) # OK get_user(order_id) # Type error! OrderId is not UserId get_user(42) # Type error! int is not UserId

NewType is zero-cost at runtime (just returns the value)

But provides type safety during static checking

Callable Types

Type hint for callable objects (functions, lambdas, callables):

from typing import Callable

Basic callable: (param_types...) -> return_type

def apply_operation(x: int, operation: Callable[[int], int]) -> int: return operation(x)

Usage

def double(n: int) -> int: return n * 2

result = apply_operation(5, double) # OK result = apply_operation(5, lambda x: x * 3) # OK

Multiple parameters

def apply_binary( x: int, y: int, operation: Callable[[int, int], int] ) -> int: return operation(x, y)

apply_binary(5, 3, lambda a, b: a + b) # OK

No parameters

def run_callback(callback: Callable[[], None]) -> None: callback()

Variable arguments (use ... for flexibility)

def log_with_formatter( message: str, formatter: Callable[..., str] ) -> None: formatted = formatter(message) print(formatted)

Callback type alias

Validator = Callable[[str], bool]

def validate_input(value: str, validator: Validator) -> bool: return validator(value)

Type Aliases

Create readable aliases for complex types:

from typing import TypeAlias

Simple alias

Vector: TypeAlias = list[float] Matrix: TypeAlias = list[Vector]

def scale_vector(vector: Vector, factor: float) -> Vector: return [x * factor for x in vector]

Complex nested types

JSON: TypeAlias = dict[str, "JSON"] | list["JSON"] | str | int | float | bool | None

def parse_json(data: str) -> JSON: import json return json.loads(data)

Union aliases

Numeric: TypeAlias = int | float OptionalString: TypeAlias = str | None

Callable alias

HandlerFunction: TypeAlias = Callable[[str, dict], None]

Generic alias

from typing import TypeVar T = TypeVar('T') Result: TypeAlias = tuple[T, str | None] # (value, error)

def safe_parse(value: str) -> Result[int]: try: return (int(value), None) except ValueError as e: return (0, str(e))

Function Annotations

Parameter Type Hints

Basic parameters

def greet(name: str, age: int) -> str: return f"{name} is {age} years old"

Default values with type hints

def greet_with_title( name: str, title: str = "Mr.", excited: bool = False ) -> str: greeting = f"{title} {name}" return greeting + "!" if excited else greeting

Multiple types (union)

def process_id(user_id: int | str) -> str: return str(user_id)

*args and **kwargs Type Hints

*args: variable positional arguments

def sum_numbers(*args: int) -> int: return sum(args)

sum_numbers(1, 2, 3, 4) # OK sum_numbers(1, 2, "3") # Type error!

**kwargs: variable keyword arguments

def configure(**kwargs: str) -> dict[str, str]: return kwargs

configure(host="localhost", port="8000") # OK configure(host="localhost", port=8000) # Type error! port must be str

Mixed args, kwargs

def complex_function( required: str, *args: int, **kwargs: bool ) -> None: pass

complex_function("test", 1, 2, 3, flag=True, debug=False) # OK

Different types for args/kwargs

from typing import Any

def flexible_function( *args: int | str, **kwargs: Any ) -> None: pass

Overload Decorator

Use @overload to provide multiple type signatures:

from typing import overload

Overload signatures (not implemented)

@overload def process(data: str) -> str: ...

@overload def process(data: int) -> int: ...

@overload def process(data: list[str]) -> list[str]: ...

Actual implementation (must be compatible with all overloads)

def process(data: str | int | list[str]) -> str | int | list[str]: if isinstance(data, str): return data.upper() elif isinstance(data, int): return data * 2 else: return [s.upper() for s in data]

Type checker knows return type based on input

result1: str = process("hello") # OK, knows it returns str result2: int = process(42) # OK, knows it returns int result3: list[str] = process(["a", "b"]) # OK, knows it returns list[str]

More complex overload example

@overload def get_value(container: dict[str, int], key: str) -> int: ...

@overload def get_value(container: list[int], key: int) -> int: ...

def get_value( container: dict[str, int] | list[int], key: str | int ) -> int: return container[key] # type: ignore[index]

Class Type Hints

Instance Variables

class User: # Instance variable annotations (PEP 526) name: str age: int email: str | None

def __init__(self, name: str, age: int, email: str | None = None) -> None:
    self.name = name
    self.age = age
    self.email = email

Alternative: annotate in init

class Product: def init(self, name: str, price: float) -> None: self.name: str = name self.price: float = price self.stock: int = 0 # Type inferred from default value

Class Variables (ClassVar)

from typing import ClassVar

class Config: # Class variable (shared across all instances) api_url: ClassVar[str] = "https://api.example.com" max_retries: ClassVar[int] = 3

# Instance variable
session_id: str

def __init__(self, session_id: str) -> None:
    self.session_id = session_id

Class variables are accessed on the class

print(Config.api_url) # OK

Type checker distinguishes class vs instance variables

config = Config("abc123") config.session_id # OK (instance variable) config.api_url # OK but mypy may warn (accessing class var from instance)

Property Type Hints

class Circle: def init(self, radius: float) -> None: self._radius = radius

@property
def radius(self) -> float:
    return self._radius

@radius.setter
def radius(self, value: float) -> None:
    if value <= 0:
        raise ValueError("Radius must be positive")
    self._radius = value

@property
def area(self) -> float:
    """Read-only property"""
    import math
    return math.pi * self._radius ** 2

Cached property

from functools import cached_property

class DataProcessor: def init(self, data: list[int]) -> None: self._data = data

@cached_property
def processed_data(self) -> list[int]:
    """Expensive computation, cached after first access"""
    return [x * 2 for x in self._data]

init Annotations

class DatabaseConnection: def init( self, host: str, port: int = 5432, username: str | None = None, password: str | None = None, *, # Force keyword-only arguments after this timeout: float = 30.0, ssl: bool = True ) -> None: self.host = host self.port = port self.username = username self.password = password self.timeout = timeout self.ssl = ssl

Usage

db = DatabaseConnection( "localhost", 5432, timeout=60.0, # Keyword-only ssl=False )

mypy Configuration

Basic mypy Setup

Create mypy.ini or pyproject.toml for mypy configuration:

mypy.ini:

[mypy]

Type checking strictness

python_version = 3.9 warn_return_any = True warn_unused_configs = True disallow_untyped_defs = True disallow_any_unimported = False no_implicit_optional = True warn_redundant_casts = True warn_unused_ignores = True warn_no_return = True check_untyped_defs = True strict_equality = True

Import discovery

namespace_packages = True ignore_missing_imports = False

Per-module options

[mypy-tests.*] disallow_untyped_defs = False

[mypy-third_party_lib.*] ignore_missing_imports = True

pyproject.toml:

[tool.mypy] python_version = "3.9" warn_return_any = true warn_unused_configs = true disallow_untyped_defs = true no_implicit_optional = true warn_redundant_casts = true warn_unused_ignores = true check_untyped_defs = true strict_equality = true

[[tool.mypy.overrides]] module = "tests.*" disallow_untyped_defs = false

[[tool.mypy.overrides]] module = "third_party_lib.*" ignore_missing_imports = true

Common mypy Flags

Strict mode (all checks enabled):

mypy --strict myfile.py

Common individual flags:

Require type hints on all functions

mypy --disallow-untyped-defs myfile.py

Warn about functions that return Any

mypy --warn-return-any myfile.py

Disallow Any types

mypy --disallow-any-expr myfile.py # Very strict!

Show error codes

mypy --show-error-codes myfile.py

Incremental mode (faster)

mypy --incremental myfile.py

Specific Python version

mypy --python-version 3.9 myfile.py

Useful combinations:

Recommended starting point

mypy --check-untyped-defs --warn-redundant-casts myfile.py

Strict for new code

mypy --strict --allow-untyped-calls myfile.py

CI/CD friendly (no incremental cache)

mypy --no-incremental --show-error-codes mypackage/

Ignoring Errors Appropriately

Use type ignore comments sparingly and document why:

Ignore specific error on one line

result = legacy_function() # type: ignore[no-untyped-call]

Ignore all errors on one line (avoid!)

bad_code = something() # type: ignore

Ignore multiple specific errors

value = complex_operation() # type: ignore[arg-type, return-value]

Ignore for entire function (last resort)

def legacy_code() -> Any: # type: ignore[no-untyped-def] pass

Better: Fix the issue or use proper typing

def proper_code() -> dict[str, Any]: """Returns config dict with various value types.""" return {"key": "value"}

When to use type: ignore:

  • Interfacing with untyped third-party libraries

  • Complex type narrowing that mypy can't infer

  • Known false positives (file a mypy issue!)

  • Temporary during gradual typing migration

When NOT to use type: ignore:

  • To avoid adding type hints

  • Because "mypy is wrong" (usually mypy is right!)

  • Without documenting the reason

  • Instead of fixing the actual type error

Type: ignore Comments Best Practices

BAD: No error code

result = func() # type: ignore

GOOD: Specific error code with explanation

mypy can't infer the narrowed type after this check

result = func() # type: ignore[arg-type] # Known false positive, see issue #123

GOOD: Document workaround

TODO: Remove once library adds type stubs

data = legacy_lib.get_data() # type: ignore[no-untyped-call]

BEST: Fix the issue instead

def properly_typed_function(x: int) -> str: return str(x)

Best Practices

  1. Gradual Typing Strategy

Start with critical code paths, expand gradually:

Phase 1: Public API functions

def public_api(user_id: int) -> dict[str, Any]: return _internal_function(user_id)

def _internal_function(user_id): # Not typed yet return {"id": user_id}

Phase 2: Expand to internal functions

def _internal_function_typed(user_id: int) -> dict[str, int]: return {"id": user_id}

Phase 3: Strict typing everywhere

def fully_typed(user_id: int) -> User: return User(id=user_id, name="Unknown")

Recommended adoption order:

  • Public API functions and methods

  • Core business logic

  • Internal utilities

  • Tests (can be less strict)

  • Scripts and one-off code (optional)

  1. When to Use Type Hints

Always type hint:

  • Public APIs and library functions

  • Function signatures (parameters and return types)

  • Class attributes and properties

  • Complex data structures

Consider type hints:

  • Internal/private functions in large codebases

  • Helper functions with non-obvious signatures

  • Callback functions

Optional type hints:

  • Very simple, obvious functions

  • Scripts and notebooks

  • Prototype code

  • One-liners and lambdas

Always type hint: Public API

def calculate_discount(price: float, discount_percent: float) -> float: """Calculate discounted price.""" return price * (1 - discount_percent / 100)

Optional: Very obvious helper

def _double(x): return x * 2

Consider: Less obvious helper (recommended)

def _format_currency(amount: float, currency: str = "USD") -> str: return f"{amount:.2f} {currency}"

  1. Type Hint Readability

Prioritize readability over exhaustive typing:

BAD: Overly complex, hard to read

def process( data: dict[str, list[tuple[int, str, dict[str, list[int | str | None]]]]] ) -> list[tuple[str, int]]: pass

GOOD: Use type aliases for readability

PersonData = dict[str, list[int | str | None]] Record = tuple[int, str, PersonData] DataDict = dict[str, list[Record]] Result = list[tuple[str, int]]

def process_readable(data: DataDict) -> Result: pass

BETTER: Use TypedDict for structured dicts

class PersonData(TypedDict): age: int name: str tags: list[str]

class Record(TypedDict): id: int type: str data: PersonData

def process_structured(data: dict[str, list[Record]]) -> Result: pass

  1. Avoiding Over-Specification

Don't type hint more than necessary:

BAD: Over-specified (too rigid)

def process_items(items: list[str]) -> list[str]: return [item.upper() for item in items]

GOOD: Accept any iterable, return list (more flexible)

from collections.abc import Iterable

def process_items_flexible(items: Iterable[str]) -> list[str]: return [item.upper() for item in items]

Now works with lists, tuples, sets, generators, etc.

process_items_flexible(["a", "b"]) # OK process_items_flexible(("a", "b")) # OK process_items_flexible(x for x in ["a", "b"]) # OK

BAD: Forces specific dict implementation

def merge(d1: dict[str, int], d2: dict[str, int]) -> dict[str, int]: return {**d1, **d2}

GOOD: Accept any mapping

from collections.abc import Mapping

def merge_flexible( d1: Mapping[str, int], d2: Mapping[str, int] ) -> dict[str, int]: return {**d1, **d2}

Use abstract types from collections.abc :

  • Iterable[T] instead of list[T] for inputs

  • Sequence[T] when you need indexing/length

  • Mapping[K, V] instead of dict[K, V] for inputs

  • MutableMapping[K, V] when you need to modify

  • Return concrete types like list , dict

  1. Using Protocol for Duck Typing

Prefer Protocol over rigid inheritance:

from typing import Protocol

BAD: Forces inheritance

class Animal: def make_sound(self) -> str: raise NotImplementedError

class Dog(Animal): # Must inherit def make_sound(self) -> str: return "Woof"

GOOD: Duck typing with type safety

class CanMakeSound(Protocol): def make_sound(self) -> str: ...

class Dog: # No inheritance needed def make_sound(self) -> str: return "Woof"

class Car: def make_sound(self) -> str: return "Vroom"

def hear_sound(thing: CanMakeSound) -> None: print(thing.make_sound())

hear_sound(Dog()) # OK hear_sound(Car()) # OK (anything with make_sound method)

Real-world example: file-like objects

class SupportsRead(Protocol): def read(self, n: int = -1) -> str: ...

def process_text_file(file: SupportsRead) -> int: content = file.read() return len(content)

Works with any object that has a read() method

import io process_text_file(io.StringIO("test")) # OK with open("file.txt") as f: process_text_file(f) # OK

  1. Type Narrowing

Help mypy understand type narrowing through conditionals:

def process_value(value: int | str | None) -> str: # mypy tracks type narrowing through conditionals

if value is None:
    return "No value"
    # After this check, value is int | str in else branch

if isinstance(value, int):
    return f"Number: {value}"
    # After this check, value is str in else branch

# mypy knows value must be str here
return f"Text: {value.upper()}"

Use isinstance for type narrowing

def double_if_number(value: int | str) -> int | str: if isinstance(value, int): # mypy knows value is int here return value * 2 # mypy knows value is str here return value

Use type guards for custom narrowing

from typing import TypeGuard

def is_list_of_strings(val: list[Any]) -> TypeGuard[list[str]]: return all(isinstance(x, str) for x in val)

def process_list(items: list[Any]) -> None: if is_list_of_strings(items): # mypy knows items is list[str] here for item in items: print(item.upper()) # OK, item is str

Common Patterns

Forward References (String Annotations)

Use string annotations for forward references:

Forward reference (class not yet defined)

class Node: def init(self, value: int, next: "Node | None" = None) -> None: self.value = value self.next = next

Python 3.10+: Can use | with quotes

class TreeNode: def init( self, value: int, left: "TreeNode | None" = None, right: "TreeNode | None" = None ) -> None: self.value = value self.left = left self.right = right

Python 3.7+: Use from future import annotations

from future import annotations

class ModernNode: def init(self, value: int, next: ModernNode | None = None) -> None: self.value = value self.next = next # No quotes needed with future import!

Circular Type Dependencies

Handle circular dependencies with TYPE_CHECKING:

from typing import TYPE_CHECKING

if TYPE_CHECKING: # Imports only used for type checking (not at runtime) from mypackage.models import User

class Post: def init(self, author: "User") -> None: self.author = author

In models.py

from typing import TYPE_CHECKING

if TYPE_CHECKING: from mypackage.posts import Post

class User: def init(self, posts: list["Post"]) -> None: self.posts = posts

Generic Container Types

Type hint containers properly:

Homogeneous lists

def process_names(names: list[str]) -> None: for name in names: print(name.upper()) # mypy knows name is str

Mixed types (use Union)

def process_mixed(items: list[int | str]) -> None: for item in items: if isinstance(item, int): print(item * 2) else: print(item.upper())

Nested containers

def process_matrix(matrix: list[list[float]]) -> float: return sum(sum(row) for row in matrix)

Dict with specific key/value types

def process_scores(scores: dict[str, float]) -> float: return sum(scores.values())

Complex nested structure

ComplexData = dict[str, list[dict[str, int | str]]]

def process_complex(data: ComplexData) -> None: pass

Callback Type Hints

Type hint callbacks and handlers:

from typing import Callable

Simple callback

def run_with_callback(callback: Callable[[int], None]) -> None: callback(42)

run_with_callback(lambda x: print(x)) # OK

Event handler

EventHandler = Callable[[str, dict[str, Any]], None]

def register_handler(event: str, handler: EventHandler) -> None: pass

def on_user_created(event_name: str, data: dict[str, Any]) -> None: print(f"Event: {event_name}, Data: {data}")

register_handler("user.created", on_user_created)

Factory function

Factory = Callable[[], T]

def create_default(factory: Factory[T]) -> T: return factory()

create_default(lambda: []) # Returns list create_default(lambda: {}) # Returns dict

Validator function

Validator = Callable[[str], bool]

def validate_email(email: str) -> bool: return "@" in email

def process_with_validation(value: str, validator: Validator) -> bool: return validator(value)

process_with_validation("test@example.com", validate_email)

Context Manager Type Hints

Type hint context managers properly:

from typing import Iterator, Generator from contextlib import contextmanager

Simple context manager class

class DatabaseConnection: def enter(self) -> "DatabaseConnection": print("Opening connection") return self

def __exit__(self, exc_type, exc_val, exc_tb) -> None:
    print("Closing connection")

Generator-based context manager

@contextmanager def open_file(path: str) -> Iterator[list[str]]: file = open(path) try: yield file.readlines() finally: file.close()

Generic context manager

from typing import Generic T = TypeVar('T')

@contextmanager def managed_resource(resource: T) -> Iterator[T]: try: yield resource finally: print(f"Cleaning up {resource}")

Async context manager

class AsyncDatabaseConnection: async def aenter(self) -> "AsyncDatabaseConnection": print("Opening async connection") return self

async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
    print("Closing async connection")

Anti-Patterns

Using Any Everywhere

BAD: Defeats purpose of type hints

def process(data: Any) -> Any: return data

def calculate(x: Any, y: Any) -> Any: return x + y

GOOD: Use specific types

def process_user(data: dict[str, str]) -> User: return User(**data)

def calculate_sum(x: int, y: int) -> int: return x + y

ACCEPTABLE: Gradual typing transition

def legacy_function(data: Any) -> dict[str, Any]: # TODO: Add proper types once interface is stable return {"result": data}

Over-Complicated Type Hints

BAD: Unreadable, overly complex

def transform( data: dict[str, list[tuple[int, str, dict[str, list[int | str | None]]]]] ) -> list[tuple[str, dict[str, list[int]]]]: pass

GOOD: Use type aliases

InputRecord = tuple[int, str, dict[str, list[int | str | None]]] InputData = dict[str, list[InputRecord]] OutputRecord = tuple[str, dict[str, list[int]]] OutputData = list[OutputRecord]

def transform_readable(data: InputData) -> OutputData: pass

BETTER: Use TypedDict or dataclasses

from dataclasses import dataclass

@dataclass class Record: id: int name: str metadata: dict[str, list[int | str | None]]

@dataclass class TransformedRecord: name: str values: dict[str, list[int]]

def transform_structured( data: dict[str, list[Record]] ) -> list[TransformedRecord]: pass

Ignoring Type Errors Without Justification

BAD: Silencing errors without understanding

result = risky_operation() # type: ignore

BAD: Generic ignore

value = complex_call() # type: ignore

GOOD: Specific error with explanation

mypy doesn't understand this runtime type check

result = risky_operation() # type: ignore[arg-type] # See issue #456

GOOD: Document temporary workaround

TODO: Remove after upgrading to library v2.0 with type stubs

value = legacy_lib.call() # type: ignore[no-untyped-call]

BEST: Fix the underlying issue

def properly_typed_operation() -> int: return 42

result = properly_typed_operation() # No ignore needed!

Not Running mypy in CI/CD

BAD: Only running mypy locally

Developers forget, type errors slip through

GOOD: CI/CD pipeline (GitHub Actions example)

.github/workflows/type-check.yml

""" name: Type Check on: [push, pull_request] jobs: mypy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - uses: actions/setup-python@v2 with: python-version: '3.9' - run: pip install mypy - run: mypy src/ """

GOOD: pre-commit hook

.pre-commit-config.yaml

""" repos:

Inconsistent Type Hint Usage

BAD: Inconsistent typing within same module

def get_user(user_id: int) -> dict: # Untyped dict return {"id": user_id}

def create_user(name, email): # No types at all return {"name": name, "email": email}

def delete_user(user_id: int) -> bool: # Typed return True

GOOD: Consistent typing throughout

class User(TypedDict): id: int name: str email: str

def get_user(user_id: int) -> User: return {"id": user_id, "name": "Unknown", "email": "unknown@example.com"}

def create_user(name: str, email: str) -> User: return {"id": 0, "name": name, "email": email}

def delete_user(user_id: int) -> bool: return True

Tools

mypy - Standard Type Checker

Install

pip install mypy

Basic usage

mypy file.py mypy src/

With config file

mypy --config-file mypy.ini src/

Show error codes

mypy --show-error-codes src/

Strict mode

mypy --strict src/

Generate HTML report

mypy --html-report mypy-report src/

Ignore missing imports

mypy --ignore-missing-imports src/

Check specific Python version

mypy --python-version 3.9 src/

Pros:

  • Official type checker

  • Excellent documentation

  • Large community

  • Extensive configuration options

Cons:

  • Can be slow on large codebases

  • Sometimes conservative type inference

pyright - Microsoft Type Checker

Install (requires Node.js)

npm install -g pyright

Or use via pip

pip install pyright

Basic usage

pyright src/

Watch mode

pyright --watch src/

VS Code integration

Install "Pylance" extension (includes pyright)

Pros:

  • Very fast

  • Excellent IDE integration (VS Code)

  • Advanced type inference

  • Good error messages

Cons:

  • Different configuration than mypy

  • Less community adoption than mypy

pyre - Facebook Type Checker

Install

pip install pyre-check

Initialize

pyre init

Run

pyre check

Incremental mode

pyre start pyre incremental

Pros:

  • Fast incremental checking

  • Good for large codebases

  • Advanced features (infer, query)

Cons:

  • Less widespread adoption

  • Primarily tested on Facebook's codebase

Type Stubs (.pyi files)

Create stub files for untyped code or third-party libraries:

mylib.pyi:

Stub file (parallel to mylib.py)

def process(data: str) -> int: ...

class Handler: def handle(self, event: str) -> None: ...

Usage:

Install type stubs for popular libraries

pip install types-requests pip install types-redis pip install types-PyYAML

Search for available stubs

pip search types-

Generate stubs from Python code

stubgen -p mypackage -o stubs/

Stub file best practices:

  • Use ... for function bodies

  • Include all public API

  • More permissive types than implementation

  • Keep in sync with actual code

Checklists

Type Hint Quality Checklist

When reviewing code for type hints, verify:

  • All public functions have parameter and return type hints

  • Type hints are accurate (match actual behavior)

  • Complex types use aliases for readability

  • No overuse of Any (each Any is justified)

  • Collection types are properly specified (e.g., list[str] not list )

  • Optional parameters use | None or Optional[]

  • *args and **kwargs are typed when used

  • Class attributes have type annotations

  • Properties have return type hints

  • No type: ignore without error code and explanation

  • Forward references use quotes or future.annotations

  • Union types use | operator (Python 3.10+) or Union[]

  • TypedDict used for structured dicts

  • Protocol used instead of forced inheritance where appropriate

mypy Configuration Checklist

For proper mypy setup, ensure:

  • mypy.ini or pyproject.toml configuration exists

  • Python version specified in config

  • warn_return_any enabled

  • warn_unused_configs enabled

  • no_implicit_optional enabled

  • warn_redundant_casts enabled

  • warn_unused_ignores enabled

  • strict_equality enabled

  • Per-module overrides for tests and third-party code

  • CI/CD pipeline runs mypy

  • Pre-commit hook for mypy (optional but recommended)

  • Team agrees on strictness level

Gradual Typing Migration Checklist

When adding types to existing codebase:

  • Start with most critical/public API functions

  • Add types to new code immediately

  • Focus on one module at a time

  • Use Any temporarily for complex migrations

  • Run mypy incrementally (not --strict initially)

  • Document known type issues in TODO comments

  • Create type stubs for untyped dependencies

  • Enable stricter mypy options gradually

  • Update documentation with type examples

  • Train team on type hint best practices

Examples

See the examples/ directory for comprehensive examples:

  • examples/basic_types.py

  • Basic type hint usage

  • examples/advanced_types.py

  • Generic types, protocols, TypedDict

  • examples/good_vs_bad.py

  • Common patterns vs anti-patterns

  • examples/mypy_config_examples/

  • Sample mypy configurations

Templates

Template: mypy.ini Configuration

Located at: templates/mypy.ini

Use this as a starting point for mypy configuration in new projects.

Template: Type Stub File

Located at: templates/stub_file.pyi

Use this template when creating type stubs for untyped code.

Template: Typed Class

Located at: templates/typed_class.py

Example of a well-typed Python class with all common patterns.

Related Skills

  • python-testing-patterns: Type hints improve testability and test clarity

  • python-code-style: Type hints are part of PEP 8 style guidelines

  • python-async-patterns: Async functions require special type hint considerations

References

PEPs (Python Enhancement Proposals)

  • PEP 484: Type Hints (foundation)

  • PEP 526: Syntax for Variable Annotations

  • PEP 544: Protocols (structural subtyping)

  • PEP 585: Type Hinting Generics In Standard Collections (Python 3.9+)

  • PEP 586: Literal Types

  • PEP 589: TypedDict

  • PEP 591: Final qualifier

  • PEP 604: Union operator | (Python 3.10+)

  • PEP 612: Parameter Specification Variables

  • PEP 613: TypeAlias annotation

  • PEP 647: Type Guards

Official Documentation

External Resources

Version: 1.0 Last Updated: 2025-12-24 Target Python Version: 3.9+ Maintainer: Uncle Duke (Python Development Agent)

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-testing-standards

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

python-async-patterns

No summary provided by upstream source.

Repository SourceNeeds Review
General

java-best-practices

No summary provided by upstream source.

Repository SourceNeeds Review
General

spring-framework-patterns

No summary provided by upstream source.

Repository SourceNeeds Review