Claude-initial-setup type-hints-guide

install
source · Clone the upstream repo
git clone https://github.com/VersoXBT/claude-initial-setup
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/VersoXBT/claude-initial-setup "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/python/type-hints-guide" ~/.claude/skills/versoxbt-claude-initial-setup-type-hints-guide && rm -rf "$T"
manifest: skills/python/type-hints-guide/SKILL.md
source content

Python Type Hints Guide

Write precise, expressive type annotations that catch bugs at static analysis time. Modern Python typing (3.10+) is powerful enough to express complex relationships without runtime overhead.

When to Use

  • User writes or refactors Python functions without type hints
  • User asks about typing, generics, or protocols
  • User encounters mypy or pyright errors
  • User builds libraries or APIs that need clear contracts
  • User works with complex data structures or callbacks

Core Patterns

Modern Built-in Types (Python 3.10+)

Prefer built-in generics and union syntax over

typing
imports.

# Python 3.10+ -- use built-in types and | union
def process_items(
    items: list[str],
    config: dict[str, int | float],
    callback: Callable[[str], None] | None = None,
) -> tuple[int, list[str]]:
    ...

# For older Python, import from typing
from typing import List, Dict, Optional, Union, Tuple

TypeVar and Generic Classes

Use TypeVar for functions/classes that work with any type while preserving type relationships.

from typing import TypeVar, Generic

T = TypeVar("T")
K = TypeVar("K")
V = TypeVar("V")

class Result(Generic[T]):
    """A container that holds either a value or an error."""

    def __init__(self, value: T | None = None, error: str | None = None):
        self._value = value
        self._error = error

    def unwrap(self) -> T:
        if self._error is not None:
            raise ValueError(self._error)
        assert self._value is not None
        return self._value

    def map(self, func: Callable[[T], V]) -> "Result[V]":
        if self._error:
            return Result(error=self._error)
        return Result(value=func(self.unwrap()))

# Bounded TypeVar -- restrict to specific types
Numeric = TypeVar("Numeric", int, float)

def add(a: Numeric, b: Numeric) -> Numeric:
    return a + b

# Upper bound -- must be subclass of
from datetime import datetime
DateLike = TypeVar("DateLike", bound=datetime)

Protocol (Structural Subtyping)

Use Protocol instead of ABCs when you want duck typing with static checking. This is Python's answer to Go-style interfaces.

from typing import Protocol, runtime_checkable

@runtime_checkable
class Renderable(Protocol):
    def render(self) -> str: ...

class SupportsComparison(Protocol):
    def __lt__(self, other: "SupportsComparison") -> bool: ...
    def __eq__(self, other: object) -> bool: ...

# Any class with a render() -> str method satisfies Renderable
# No inheritance needed
class HtmlWidget:
    def render(self) -> str:
        return "<div>widget</div>"

def display(item: Renderable) -> None:
    print(item.render())

display(HtmlWidget())  # Works -- structural match

ParamSpec (Preserving Function Signatures)

Use ParamSpec for decorators that preserve the wrapped function's signature.

from typing import ParamSpec, TypeVar, Callable
import functools

P = ParamSpec("P")
R = TypeVar("R")

def log_calls(func: Callable[P, R]) -> Callable[P, R]:
    """Decorator that logs function calls while preserving type info."""
    @functools.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 greet(name: str, greeting: str = "Hello") -> str:
    return f"{greeting}, {name}!"

# Type checker knows: greet(name: str, greeting: str = "Hello") -> str

Literal, TypeAlias, and Overload

from typing import Literal, TypeAlias, overload

# Literal -- restrict to exact values
Direction: TypeAlias = Literal["north", "south", "east", "west"]
LogLevel: TypeAlias = Literal["debug", "info", "warning", "error"]

def set_log_level(level: LogLevel) -> None:
    ...

# TypeAlias -- name complex types
JsonPrimitive: TypeAlias = str | int | float | bool | None
JsonValue: TypeAlias = JsonPrimitive | list["JsonValue"] | dict[str, "JsonValue"]
UserHandler: TypeAlias = Callable[[str, dict[str, str]], "Response"]

# Overload -- different return types based on input
@overload
def parse(raw: str, as_list: Literal[True]) -> list[str]: ...
@overload
def parse(raw: str, as_list: Literal[False] = ...) -> str: ...

def parse(raw: str, as_list: bool = False) -> str | list[str]:
    if as_list:
        return raw.split(",")
    return raw.strip()

Dataclass Field Typing

from dataclasses import dataclass, field
from typing import ClassVar

@dataclass
class Config:
    # Required fields
    host: str
    port: int

    # Optional with defaults
    debug: bool = False
    tags: list[str] = field(default_factory=list)

    # ClassVar -- not included in __init__ or __repr__
    MAX_RETRIES: ClassVar[int] = 3

    # InitVar -- passed to __init__ but not stored as field
    from dataclasses import InitVar
    password: InitVar[str] = ""

    def __post_init__(self, password: str) -> None:
        self._hashed = hash(password) if password else 0

TypeGuard and TypeIs (Narrowing)

from typing import TypeGuard, TypeIs

def is_string_list(val: list[object]) -> TypeGuard[list[str]]:
    """Narrow list[object] to list[str]."""
    return all(isinstance(item, str) for item in val)

# Python 3.13+ TypeIs -- narrower and more precise than TypeGuard
def is_int(val: int | str) -> TypeIs[int]:
    return isinstance(val, int)

def process(val: int | str) -> None:
    if is_int(val):
        # val is narrowed to int
        print(val + 1)
    else:
        # val is narrowed to str (TypeIs does this, TypeGuard does not)
        print(val.upper())

Anti-Patterns

  • Using
    Any
    to silence type errors
    : Fix the actual type issue.
    Any
    disables all checking and defeats the purpose.
  • Optional[X]
    confusion
    :
    Optional[X]
    means
    X | None
    , not "this parameter is optional." Use
    X | None
    for clarity (Python 3.10+).
  • Forgetting
    @runtime_checkable
    on Protocols
    : Without it,
    isinstance()
    checks against the Protocol will fail at runtime.
  • Over-generic TypeVars: If a TypeVar is only used once in a signature, it adds no value. Use the concrete type or
    object
    instead.
  • Ignoring
    # type: ignore
    comments
    : Each suppression should have a specific error code:
    # type: ignore[assignment]
    . Never use bare
    # type: ignore
    .

Quick Reference

ConstructPurpose
T = TypeVar("T")
Generic type parameter
TypeVar("T", bound=Base)
Upper-bounded generic
Protocol
Structural subtyping (duck typing)
ParamSpec("P")
Preserve callable signatures
Literal["a", "b"]
Exact value types
TypeAlias
Name complex type expressions
@overload
Multiple return types by input
TypeGuard[X]
Custom type narrowing
ClassVar[int]
Class-level (not instance) field
Final[str]
Immutable binding