Claude Skill

python-typing-ops

Python type hints and type safety patterns. Triggers on: type hints, typing, TypeVar, Generic, Protocol, mypy, pyright, type annotation, overload, TypedDict.

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download 0xdarkmatter-claude-mods-skills_python-typing-ops-3dfaf0b.zip · 17 KB
Part of 0xdarkmatter/claude-mods — 94 skills

Install

skills CLI npx skills add https://github.com/0xDarkMatter/claude-mods/tree/main/skills/python-typing-ops
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install 0xdarkmatter-claude-mods@llmmart
Git git clone https://github.com/0xDarkMatter/claude-mods.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole 0xdarkmatter/claude-mods collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

Python Typing Patterns

Modern type hints for safe, documented Python code.

Basic Annotations

# Variables
name: str = "Alice"
count: int = 42
items: list[str] = ["a", "b"]
mapping: dict[str, int] = {"key": 1}

# Function signatures
def greet(name: str, times: int = 1) -> str:
    return f"Hello, {name}!" * times

# None handling
def find(id: int) -> str | None:
    return db.get(id)  # May return None

Collections

from collections.abc import Sequence, Mapping, Iterable

# Use collection ABCs for flexibility
def process(items: Sequence[str]) -> list[str]:
    """Accepts list, tuple, or any sequence."""
    return [item.upper() for item in items]

def lookup(data: Mapping[str, int], key: str) -> int:
    """Accepts dict or any mapping."""
    return data.get(key, 0)

# Nested types
Matrix = list[list[float]]
Config = dict[str, str | int | bool]

Optional and Union

# Modern syntax (3.10+)
def find(id: int) -> User | None:
    pass

def parse(value: str | int | float) -> str:
    pass

# With default None
def fetch(url: str, timeout: float | None = None) -> bytes:
    pass

TypedDict

from typing import TypedDict, Required, NotRequired

class UserDict(TypedDict):
    id: int
    name: str
    email: str | None

class ConfigDict(TypedDict, total=False):  # All optional
    debug: bool
    log_level: str

class APIResponse(TypedDict):
    data: Required[list[dict]]
    error: NotRequired[str]

def process_user(user: UserDict) -> str:
    return user["name"]  # Type-safe key access

Callable

from collections.abc import Callable

# Function type
Handler = Callable[[str, int], bool]

def register(callback: Callable[[str], None]) -> None:
    pass

# With keyword args (use Protocol instead)
from typing import Protocol

class Processor(Protocol):
    def __call__(self, data: str, *, verbose: bool = False) -> int:
        ...

Generics

from typing import TypeVar

T = TypeVar("T")

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

# Bounded TypeVar
from typing import SupportsFloat

N = TypeVar("N", bound=SupportsFloat)

def average(values: list[N]) -> float:
    return sum(float(v) for v in values) / len(values)

Protocol (Structural Typing)

from typing import Protocol

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

def load(source: Readable) -> dict:
    """Accepts any object with read() method."""
    data = source.read()
    return json.loads(data)

# Works with file, BytesIO, custom classes
load(open("data.json", "rb"))
load(io.BytesIO(b"{}"))

Type Guards

from typing import TypeGuard

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]
        print(", ".join(items))

Literal and Final

from typing import Literal, Final

Mode = Literal["read", "write", "append"]

def open_file(path: str, mode: Mode) -> None:
    pass

# Constants
MAX_SIZE: Final = 1024
API_VERSION: Final[str] = "v2"

Quick Reference

Type Use Case
X \| None Optional value
list[T] Homogeneous list
dict[K, V] Dictionary
Callable[[Args], Ret] Function type
TypeVar("T") Generic parameter
Protocol Structural typing
TypedDict Dict with fixed keys
Literal["a", "b"] Specific values only
Final Cannot be reassigned

Type Checker Commands

# mypy (run inside the project env)
uv run mypy src/ --strict

# pyright
uv run pyright src/

# In pyproject.toml
[tool.mypy]
strict = true
python_version = "3.11"

Emerging: ty — Astral's Rust-based type checker (same toolchain as uv + ruff), dramatically faster than mypy. Still in preview (pre-1.0), so mypy or pyright remain the production default — but worth watching, and easy to try: uvx ty check. Adopt for new projects once it stabilizes.

Additional Resources

  • ./references/generics-advanced.md - TypeVar, ParamSpec, TypeVarTuple
  • ./references/protocols-patterns.md - Structural typing, runtime protocols
  • ./references/type-narrowing.md - Guards, isinstance, assert
  • ./references/mypy-config.md - mypy/pyright configuration
  • ./references/runtime-validation.md - Pydantic v2, typeguard, beartype
  • ./references/overloads.md - @overload decorator patterns

Scripts

  • ./scripts/check-types.sh - Run type checkers with common options

Assets

  • ./assets/pyproject-typing.toml - Recommended mypy/pyright config

See Also

This is a foundation skill with no prerequisites.

Related Skills:

  • python-pytest-ops - Type-safe fixtures and mocking

Build on this skill:

  • python-async-ops - Async type annotations
  • python-fastapi-ops - Pydantic models and validation
  • python-database-ops - SQLAlchemy type annotations
Files (claude-mods)
  • assets
    • pyproject-typing.toml 2.5 KB
      # pyproject.toml - Type checker configuration
      # Copy these sections to your pyproject.toml
      
      # ============================================================
      # mypy Configuration
      # ============================================================
      
      [tool.mypy]
      # Python version to target
      python_version = "3.11"
      
      # Enable strict mode (recommended for new projects)
      strict = true
      
      # Additional strictness
      warn_return_any = true
      warn_unused_ignores = true
      warn_unreachable = true
      
      # Error reporting
      show_error_codes = true
      show_error_context = true
      show_column_numbers = true
      pretty = true
      
      # Paths
      files = ["src", "tests"]
      exclude = [
          "migrations/",
          "venv/",
          ".venv/",
          "__pycache__/",
          "build/",
          "dist/",
      ]
      
      # Plugin support (uncomment as needed)
      # plugins = [
      #     "pydantic.mypy",
      #     "sqlalchemy.ext.mypy.plugin",
      # ]
      
      # ============================================================
      # Per-module overrides
      # ============================================================
      
      # Relax strictness for tests
      [[tool.mypy.overrides]]
      module = "tests.*"
      disallow_untyped_defs = false
      disallow_untyped_calls = false
      
      # Ignore missing stubs for common libraries
      [[tool.mypy.overrides]]
      module = [
          "requests.*",
          "boto3.*",
          "botocore.*",
          "celery.*",
          "redis.*",
      ]
      ignore_missing_imports = true
      
      # Legacy code - gradually add types
      # [[tool.mypy.overrides]]
      # module = "legacy.*"
      # ignore_errors = true
      
      
      # ============================================================
      # pyright Configuration
      # ============================================================
      
      [tool.pyright]
      # Python version
      pythonVersion = "3.11"
      
      # Paths
      include = ["src"]
      exclude = [
          "**/node_modules",
          "**/__pycache__",
          "venv",
          ".venv",
          "build",
          "dist",
      ]
      
      # Type checking mode: off, basic, standard, strict
      typeCheckingMode = "strict"
      
      # Report settings (strict mode enables all by default)
      reportMissingTypeStubs = false
      reportUnusedImport = "warning"
      reportUnusedVariable = "warning"
      reportUnusedFunction = "warning"
      
      # Useful additional checks
      reportUninitializedInstanceVariable = true
      reportIncompatibleMethodOverride = true
      reportIncompatibleVariableOverride = true
      
      
      # ============================================================
      # Recommended dev dependencies
      # ============================================================
      
      # [project.optional-dependencies]
      # dev = [
      #     "mypy>=1.8.0",
      #     "pyright>=1.1.350",
      #     # Common type stubs
      #     "types-requests",
      #     "types-redis",
      #     "types-PyYAML",
      #     "types-python-dateutil",
      # ]
      
  • references
    • generics-advanced.md 6.3 KB
      # Advanced Generics
      
      Deep dive into Python's generic type system.
      
      ## TypeVar Basics
      
      ```python
      from typing import TypeVar
      
      # Unconstrained TypeVar
      T = TypeVar("T")
      
      def identity(x: T) -> T:
          return x
      
      # Usage - type is preserved
      reveal_type(identity(42))      # int
      reveal_type(identity("hello")) # str
      ```
      
      ## Bounded TypeVar
      
      ```python
      from typing import TypeVar
      
      # Upper bound - T must be subtype of bound
      class Animal:
          def speak(self) -> str:
              return "..."
      
      class Dog(Animal):
          def speak(self) -> str:
              return "woof"
      
      A = TypeVar("A", bound=Animal)
      
      def make_speak(animal: A) -> A:
          print(animal.speak())
          return animal
      
      # Works with Animal or any subclass
      dog = make_speak(Dog())  # Returns Dog, not Animal
      ```
      
      ## Constrained TypeVar
      
      ```python
      from typing import TypeVar
      
      # Constrained to specific types
      StrOrBytes = TypeVar("StrOrBytes", str, bytes)
      
      def concat(a: StrOrBytes, b: StrOrBytes) -> StrOrBytes:
          return a + b
      
      # Must be same type
      concat("a", "b")     # OK -> str
      concat(b"a", b"b")   # OK -> bytes
      # concat("a", b"b")  # Error: can't mix
      ```
      
      ## Generic Classes
      
      ```python
      from typing import Generic, TypeVar
      
      T = TypeVar("T")
      
      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:
              return self._items.pop()
      
          def peek(self) -> T | None:
              return self._items[-1] if self._items else None
      
      # Usage
      int_stack: Stack[int] = Stack()
      int_stack.push(1)
      int_stack.push(2)
      value = int_stack.pop()  # int
      
      str_stack: Stack[str] = Stack()
      str_stack.push("hello")
      ```
      
      ## Multiple Type Parameters
      
      ```python
      from typing import Generic, TypeVar
      
      K = TypeVar("K")
      V = TypeVar("V")
      
      class Pair(Generic[K, V]):
          def __init__(self, key: K, value: V) -> None:
              self.key = key
              self.value = value
      
          def swap(self) -> "Pair[V, K]":
              return Pair(self.value, self.key)
      
      pair: Pair[str, int] = Pair("age", 30)
      swapped = pair.swap()  # Pair[int, str]
      ```
      
      ## Self Type (Python 3.11+)
      
      ```python
      from typing import Self
      
      class Builder:
          def __init__(self) -> None:
              self.value = ""
      
          def add(self, text: str) -> Self:
              self.value += text
              return self
      
          def build(self) -> str:
              return self.value
      
      class HTMLBuilder(Builder):
          def tag(self, name: str) -> Self:
              self.value = f"<{name}>{self.value}</{name}>"
              return self
      
      # Chaining works with correct types
      html = HTMLBuilder().add("Hello").tag("p").build()
      ```
      
      ## ParamSpec (Python 3.10+)
      
      ```python
      from typing import ParamSpec, TypeVar, Callable
      
      P = ParamSpec("P")
      R = TypeVar("R")
      
      def with_logging(func: Callable[P, R]) -> Callable[P, R]:
          """Decorator that preserves function signature."""
          def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
              print(f"Calling {func.__name__}")
              return func(*args, **kwargs)
          return wrapper
      
      @with_logging
      def greet(name: str, excited: bool = False) -> str:
          return f"Hello, {name}{'!' if excited else '.'}"
      
      # Signature preserved:
      greet("Alice", excited=True)  # OK
      # greet(123)  # Type error
      ```
      
      ## TypeVarTuple (Python 3.11+)
      
      ```python
      from typing import TypeVarTuple, Unpack
      
      Ts = TypeVarTuple("Ts")
      
      def concat_tuples(
          a: tuple[*Ts],
          b: tuple[*Ts]
      ) -> tuple[*Ts, *Ts]:
          return (*a, *b)
      
      # Usage
      result = concat_tuples((1, "a"), (2, "b"))
      # result: tuple[int, str, int, str]
      ```
      
      ## Covariance and Contravariance
      
      ```python
      from typing import TypeVar
      
      # Covariant: Can use subtype
      T_co = TypeVar("T_co", covariant=True)
      
      class Reader(Generic[T_co]):
          def read(self) -> T_co:
              ...
      
      # Contravariant: Can use supertype
      T_contra = TypeVar("T_contra", contravariant=True)
      
      class Writer(Generic[T_contra]):
          def write(self, value: T_contra) -> None:
              ...
      
      # Invariant (default): Must be exact type
      T = TypeVar("T")  # Invariant
      
      class Container(Generic[T]):
          def get(self) -> T:
              ...
          def set(self, value: T) -> None:
              ...
      ```
      
      ## Generic Protocols
      
      ```python
      from typing import Protocol, TypeVar
      
      T = TypeVar("T")
      
      class Comparable(Protocol[T]):
          def __lt__(self, other: T) -> bool:
              ...
          def __gt__(self, other: T) -> bool:
              ...
      
      def max_value(a: T, b: T) -> T:
          return a if a > b else b
      
      # Works with any comparable type
      max_value(1, 2)        # int
      max_value("a", "b")    # str
      ```
      
      ## Type Aliases
      
      ```python
      from typing import TypeAlias
      
      # Simple alias
      Vector: TypeAlias = list[float]
      Matrix: TypeAlias = list[Vector]
      
      # Generic alias
      from typing import TypeVar
      
      T = TypeVar("T")
      Result: TypeAlias = tuple[T, str | None]
      
      def parse(data: str) -> Result[int]:
          try:
              return (int(data), None)
          except ValueError as e:
              return (0, str(e))
      ```
      
      ## NewType
      
      ```python
      from typing import NewType
      
      # Create distinct types for type safety
      UserId = NewType("UserId", int)
      OrderId = NewType("OrderId", int)
      
      def get_user(user_id: UserId) -> dict:
          ...
      
      def get_order(order_id: OrderId) -> dict:
          ...
      
      user_id = UserId(42)
      order_id = OrderId(42)
      
      get_user(user_id)   # OK
      # get_user(order_id)  # Type error!
      # get_user(42)        # Type error!
      ```
      
      ## PEP 695 Type Parameter Syntax (Python 3.12+)
      
      ```python
      # No explicit TypeVar declaration needed
      def first[T](items: list[T]) -> T | None:
          return items[0] if items else None
      
      class Stack[T]:
          def push(self, item: T) -> None: ...
      
      # Bounded type parameters
      def largest[T: (int, float)](items: list[T]) -> T:
          return max(items)
      
      # Type alias statement replaces TypeAlias
      type Vector = list[float]
      type Result[T] = tuple[T, str | None]
      ```
      
      ## Override Decorator (Python 3.12+)
      
      ```python
      from typing import override
      
      class Parent:
          def greet(self) -> str:
              return "Hello"
      
      class Child(Parent):
          @override
          def greet(self) -> str:  # Type checker errors if Parent.greet is renamed
              return "Hi"
      ```
      
      ## Best Practices
      
      1. **Name TypeVars descriptively** - `T`, `K`, `V` for simple cases; `ItemT`, `KeyT` for complex
      2. **Use bounds** - When you need method access on type parameter
      3. **Prefer Protocol** - Over ABC for structural typing
      4. **Use Self** - Instead of quoted class names in return types
      5. **Covariance** - For read-only containers
      6. **Contravariance** - For write-only/function parameter types
      7. **Invariance** - For mutable containers (default, usually correct)
      
    • mypy-config.md 5.8 KB
      # mypy and pyright Configuration
      
      Type checker setup for strict, practical type safety.
      
      ## mypy Configuration
      
      ### pyproject.toml (Recommended)
      
      ```toml
      [tool.mypy]
      python_version = "3.11"
      strict = true
      warn_return_any = true
      warn_unused_ignores = true
      show_error_codes = true
      show_error_context = true
      
      # Paths
      files = ["src", "tests"]
      exclude = [
          "migrations/",
          "venv/",
          "__pycache__/",
      ]
      
      # Per-module overrides
      [[tool.mypy.overrides]]
      module = "tests.*"
      disallow_untyped_defs = false
      
      [[tool.mypy.overrides]]
      module = [
          "requests.*",
          "boto3.*",
          "botocore.*",
      ]
      ignore_missing_imports = true
      ```
      
      ### mypy.ini (Alternative)
      
      ```ini
      [mypy]
      python_version = 3.11
      strict = True
      warn_return_any = True
      warn_unused_ignores = True
      show_error_codes = True
      
      [mypy-tests.*]
      disallow_untyped_defs = False
      
      [mypy-requests.*]
      ignore_missing_imports = True
      ```
      
      ## mypy Flags Explained
      
      ### Strict Mode Components
      
      ```toml
      [tool.mypy]
      # strict = true enables all of these:
      warn_unused_configs = true
      disallow_any_generics = true
      disallow_subclassing_any = true
      disallow_untyped_calls = true
      disallow_untyped_defs = true
      disallow_incomplete_defs = true
      check_untyped_defs = true
      disallow_untyped_decorators = true
      warn_redundant_casts = true
      warn_unused_ignores = true
      warn_return_any = true
      no_implicit_reexport = true
      strict_equality = true
      extra_checks = true
      ```
      
      ### Commonly Adjusted Flags
      
      ```toml
      [tool.mypy]
      # Allow untyped defs in some files
      disallow_untyped_defs = true
      
      # But not for tests
      [[tool.mypy.overrides]]
      module = "tests.*"
      disallow_untyped_defs = false
      
      # Ignore third-party stubs
      ignore_missing_imports = true  # Global fallback
      
      # Show where errors occur
      show_error_context = true
      show_column_numbers = true
      show_error_codes = true
      
      # Error output format
      pretty = true
      ```
      
      ## pyright Configuration
      
      ### pyrightconfig.json
      
      ```json
      {
        "include": ["src"],
        "exclude": ["**/node_modules", "**/__pycache__", "venv"],
        "pythonVersion": "3.11",
        "pythonPlatform": "All",
        "typeCheckingMode": "strict",
        "reportMissingImports": true,
        "reportMissingTypeStubs": false,
        "reportUnusedImport": true,
        "reportUnusedClass": true,
        "reportUnusedFunction": true,
        "reportUnusedVariable": true,
        "reportDuplicateImport": true,
        "reportPrivateUsage": true,
        "reportConstantRedefinition": true,
        "reportIncompatibleMethodOverride": true,
        "reportIncompatibleVariableOverride": true,
        "reportInconsistentConstructor": true,
        "reportOverlappingOverload": true,
        "reportUninitializedInstanceVariable": true
      }
      ```
      
      ### pyproject.toml (pyright)
      
      ```toml
      [tool.pyright]
      include = ["src"]
      exclude = ["**/node_modules", "**/__pycache__", "venv"]
      pythonVersion = "3.11"
      typeCheckingMode = "strict"
      reportMissingTypeStubs = false
      ```
      
      ## Type Checking Modes
      
      ### pyright Modes
      
      ```json
      {
        "typeCheckingMode": "off"    // No checking
        "typeCheckingMode": "basic"  // Basic checks
        "typeCheckingMode": "standard" // Standard checks
        "typeCheckingMode": "strict"  // All checks enabled
      }
      ```
      
      ## Inline Type Ignores
      
      ```python
      # Ignore specific error
      result = some_call()  # type: ignore[arg-type]
      
      # Ignore all errors on line
      result = some_call()  # type: ignore
      
      # With mypy error code
      value = data["key"]  # type: ignore[typeddict-item]
      
      # With pyright
      result = func()  # pyright: ignore[reportGeneralTypeIssues]
      ```
      
      ## Type Stub Files (.pyi)
      
      ```python
      # mymodule.pyi - Type stubs for mymodule.py
      
      def process(data: dict[str, int]) -> list[int]: ...
      
      class Handler:
          def __init__(self, name: str) -> None: ...
          def handle(self, event: Event) -> bool: ...
      ```
      
      ### Stub Package Structure
      
      ```
      stubs/
      ├── mypackage/
      │   ├── __init__.pyi
      │   ├── module.pyi
      │   └── subpackage/
      │       └── __init__.pyi
      ```
      
      ```toml
      [tool.mypy]
      mypy_path = "stubs"
      ```
      
      ## CI Integration
      
      ### GitHub Actions
      
      ```yaml
      name: Type Check
      
      on: [push, pull_request]
      
      jobs:
        mypy:
          runs-on: ubuntu-latest
          steps:
            - uses: actions/checkout@v4
            - name: Install uv
              uses: astral-sh/setup-uv@v5
      
            - name: Install dependencies
              run: uv sync
      
            - name: Run mypy
              run: uv run mypy src/
      
        pyright:
          runs-on: ubuntu-latest
          steps:
            - uses: actions/checkout@v4
            - name: Install uv
              uses: astral-sh/setup-uv@v5
      
            - name: Install dependencies
              run: uv sync
      
            - name: Run pyright
              uses: jakebailey/pyright-action@v2
      ```
      
      ### Pre-commit Hook
      
      ```yaml
      # .pre-commit-config.yaml
      repos:
        - repo: https://github.com/pre-commit/mirrors-mypy
          rev: v1.8.0
          hooks:
            - id: mypy
              additional_dependencies: [types-requests]
              args: [--strict]
      ```
      
      ## Common Type Stubs
      
      ```bash
      # Install type stubs (dev-only)
      uv add --dev types-requests types-redis types-PyYAML "boto3-stubs[essential]"
      
      # Or use mypy to find missing stubs
      uv run mypy --install-types src/
      ```
      
      ## Gradual Typing Strategy
      
      ### Phase 1: Basic
      
      ```toml
      [tool.mypy]
      python_version = "3.11"
      warn_return_any = true
      warn_unused_ignores = true
      ```
      
      ### Phase 2: Stricter
      
      ```toml
      [tool.mypy]
      python_version = "3.11"
      disallow_untyped_defs = true
      disallow_incomplete_defs = true
      check_untyped_defs = true
      ```
      
      ### Phase 3: Strict
      
      ```toml
      [tool.mypy]
      python_version = "3.11"
      strict = true
      
      # Temporarily ignore problem areas
      [[tool.mypy.overrides]]
      module = "legacy.*"
      ignore_errors = true
      ```
      
      ## Quick Reference
      
      | mypy Flag | Description |
      |-----------|-------------|
      | `--strict` | Enable all strict checks |
      | `--show-error-codes` | Show error codes for ignores |
      | `--ignore-missing-imports` | Skip untyped libraries |
      | `--python-version 3.11` | Target Python version |
      | `--install-types` | Install missing stubs |
      | `--config-file` | Specify config file |
      
      | pyright Mode | Description |
      |--------------|-------------|
      | `off` | No checking |
      | `basic` | Minimal checks |
      | `standard` | Recommended |
      | `strict` | All checks |
      
    • overloads.md 6.3 KB
      # Function Overloads
      
      Type-safe function signatures with @overload.
      
      ## Basic Overloads
      
      ```python
      from typing import overload, Literal
      
      # Overload signatures (no implementation)
      @overload
      def process(data: str) -> str: ...
      
      @overload
      def process(data: bytes) -> bytes: ...
      
      @overload
      def process(data: int) -> int: ...
      
      # Actual implementation
      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
      
      
      # Type checker knows the return type
      result = process("hello")  # str
      result = process(b"hello")  # bytes
      result = process(42)  # int
      ```
      
      ## Overloads with Literal
      
      ```python
      from typing import overload, 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:
          response = requests.get(url)
          if format == "json":
              return response.json()
          elif format == "text":
              return response.text
          else:
              return response.content
      
      
      # Usage - return type is known
      data = fetch("https://api.example.com", "json")  # dict
      text = fetch("https://api.example.com", "text")  # str
      ```
      
      ## Overloads with Optional Parameters
      
      ```python
      from typing import overload
      
      @overload
      def get_user(user_id: int) -> User: ...
      
      @overload
      def get_user(user_id: int, include_posts: Literal[True]) -> UserWithPosts: ...
      
      @overload
      def get_user(user_id: int, include_posts: Literal[False]) -> User: ...
      
      def get_user(user_id: int, include_posts: bool = False) -> User | UserWithPosts:
          user = db.get_user(user_id)
          if include_posts:
              user.posts = db.get_posts(user_id)
              return UserWithPosts(**user.__dict__)
          return user
      
      
      # Type-safe usage
      user = get_user(1)  # User
      user_with_posts = get_user(1, include_posts=True)  # UserWithPosts
      ```
      
      ## Overloads with None Returns
      
      ```python
      from typing import overload
      
      @overload
      def find(items: list[T], predicate: Callable[[T], bool]) -> T | None: ...
      
      @overload
      def find(items: list[T], predicate: Callable[[T], bool], default: T) -> T: ...
      
      def find(
          items: list[T],
          predicate: Callable[[T], bool],
          default: T | None = None
      ) -> T | None:
          for item in items:
              if predicate(item):
                  return item
          return default
      
      
      # Without default - might be None
      result = find([1, 2, 3], lambda x: x > 5)  # int | None
      
      # With default - never None
      result = find([1, 2, 3], lambda x: x > 5, default=0)  # int
      ```
      
      ## Class Method Overloads
      
      ```python
      from typing import overload, Self
      from dataclasses import dataclass
      
      @dataclass
      class Point:
          x: float
          y: float
      
          @overload
          @classmethod
          def from_tuple(cls, coords: tuple[float, float]) -> Self: ...
      
          @overload
          @classmethod
          def from_tuple(cls, coords: tuple[float, float, float]) -> "Point3D": ...
      
          @classmethod
          def from_tuple(cls, coords: tuple[float, ...]) -> "Point | Point3D":
              if len(coords) == 2:
                  return cls(coords[0], coords[1])
              elif len(coords) == 3:
                  return Point3D(coords[0], coords[1], coords[2])
              raise ValueError("Expected 2 or 3 coordinates")
      ```
      
      ## Overloads with Generics
      
      ```python
      from typing import overload, TypeVar, Sequence
      
      T = TypeVar("T")
      K = TypeVar("K")
      V = TypeVar("V")
      
      @overload
      def first(items: Sequence[T]) -> T | None: ...
      
      @overload
      def first(items: Sequence[T], default: T) -> T: ...
      
      def first(items: Sequence[T], default: T | None = None) -> T | None:
          return items[0] if items else default
      
      
      @overload
      def get(d: dict[K, V], key: K) -> V | None: ...
      
      @overload
      def get(d: dict[K, V], key: K, default: V) -> V: ...
      
      def get(d: dict[K, V], key: K, default: V | None = None) -> V | None:
          return d.get(key, default)
      ```
      
      ## Async Overloads
      
      ```python
      from typing import overload
      
      @overload
      async def fetch_data(url: str, as_json: Literal[True]) -> dict: ...
      
      @overload
      async def fetch_data(url: str, as_json: Literal[False] = False) -> str: ...
      
      async def fetch_data(url: str, as_json: bool = False) -> dict | str:
          async with aiohttp.ClientSession() as session:
              async with session.get(url) as response:
                  if as_json:
                      return await response.json()
                  return await response.text()
      ```
      
      ## Property Overloads (Getter/Setter)
      
      ```python
      from typing import overload
      
      class Temperature:
          def __init__(self, celsius: float):
              self._celsius = celsius
      
          @property
          def value(self) -> float:
              return self._celsius
      
          @overload
          def convert(self, unit: Literal["C"]) -> float: ...
      
          @overload
          def convert(self, unit: Literal["F"]) -> float: ...
      
          @overload
          def convert(self, unit: Literal["K"]) -> float: ...
      
          def convert(self, unit: str) -> float:
              if unit == "C":
                  return self._celsius
              elif unit == "F":
                  return self._celsius * 9/5 + 32
              elif unit == "K":
                  return self._celsius + 273.15
              raise ValueError(f"Unknown unit: {unit}")
      ```
      
      ## Common Patterns
      
      ```python
      from typing import overload, Literal, TypeVar
      
      T = TypeVar("T")
      
      # Pattern 1: Return type based on flag
      @overload
      def parse(data: str, strict: Literal[True]) -> Result: ...
      @overload
      def parse(data: str, strict: Literal[False] = False) -> Result | None: ...
      
      # Pattern 2: Different return for different input types
      @overload
      def normalize(value: str) -> str: ...
      @overload
      def normalize(value: list[str]) -> list[str]: ...
      @overload
      def normalize(value: dict[str, str]) -> dict[str, str]: ...
      
      # Pattern 3: Optional vs required parameter
      @overload
      def create(name: str) -> Item: ...
      @overload
      def create(name: str, *, template: str) -> Item: ...
      ```
      
      ## Quick Reference
      
      | Pattern | Use Case |
      |---------|----------|
      | `@overload` | Define signature (no body) |
      | `Literal["value"]` | Specific string/int values |
      | `T \| None` vs `T` | Optional default changes return |
      | Implementation | Must handle all overload cases |
      
      | Rule | Description |
      |------|-------------|
      | No body in overloads | Use `...` (ellipsis) |
      | Implementation last | After all overloads |
      | Cover all cases | Implementation must accept all overload inputs |
      | Static only | Overloads are for type checkers, not runtime |
      
    • protocols-patterns.md 6.1 KB
      # Protocol Patterns
      
      Structural typing with Protocol for flexible, decoupled code.
      
      ## Basic Protocol
      
      ```python
      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")
      
      def render(shape: Drawable) -> None:
          shape.draw()
      
      # Both work - no inheritance needed
      render(Circle())
      render(Square())
      ```
      
      ## Protocol with Attributes
      
      ```python
      from typing import Protocol
      
      class Named(Protocol):
          name: str
      
      class HasId(Protocol):
          id: int
          name: str
      
      class User:
          def __init__(self, id: int, name: str):
              self.id = id
              self.name = name
      
      def greet(entity: Named) -> str:
          return f"Hello, {entity.name}"
      
      # Works with any object having 'name' attribute
      greet(User(1, "Alice"))
      ```
      
      ## Protocol with Methods
      
      ```python
      from typing import Protocol
      
      class Closeable(Protocol):
          def close(self) -> None:
              ...
      
      class Flushable(Protocol):
          def flush(self) -> None:
              ...
      
      class CloseableAndFlushable(Closeable, Flushable, Protocol):
          """Combined protocol."""
          pass
      
      def cleanup(resource: CloseableAndFlushable) -> None:
          resource.flush()
          resource.close()
      ```
      
      ## Callable Protocol
      
      ```python
      from typing import Protocol
      
      class Comparator(Protocol):
          def __call__(self, a: int, b: int) -> int:
              """Return negative, zero, or positive."""
              ...
      
      def sort_with(items: list[int], cmp: Comparator) -> list[int]:
          return sorted(items, key=lambda x: cmp(x, 0))
      
      # Lambda works
      sort_with([3, 1, 2], lambda a, b: a - b)
      
      # Function works
      def compare(a: int, b: int) -> int:
          return a - b
      
      sort_with([3, 1, 2], compare)
      ```
      
      ## Generic Protocol
      
      ```python
      from typing import Protocol, TypeVar
      
      T = TypeVar("T")
      
      class Container(Protocol[T]):
          def get(self) -> T:
              ...
      
          def set(self, value: T) -> None:
              ...
      
      class Box:
          def __init__(self, value: int):
              self._value = value
      
          def get(self) -> int:
              return self._value
      
          def set(self, value: int) -> None:
              self._value = value
      
      def process(container: Container[int]) -> int:
          value = container.get()
          container.set(value * 2)
          return container.get()
      
      process(Box(5))  # Returns 10
      ```
      
      ## Runtime Checkable Protocol
      
      ```python
      from typing import Protocol, runtime_checkable
      
      @runtime_checkable
      class Sized(Protocol):
          def __len__(self) -> int:
              ...
      
      # Now isinstance() works
      def process(obj: object) -> int:
          if isinstance(obj, Sized):
              return len(obj)
          return 0
      
      process([1, 2, 3])  # 3
      process("hello")     # 5
      process(42)          # 0
      ```
      
      ## Protocol vs ABC
      
      ```python
      from abc import ABC, abstractmethod
      from typing import Protocol
      
      # ABC - Requires explicit inheritance
      class AbstractReader(ABC):
          @abstractmethod
          def read(self) -> str:
              pass
      
      class FileReader(AbstractReader):  # Must inherit
          def read(self) -> str:
              return "content"
      
      # Protocol - Structural (duck typing)
      class ReaderProtocol(Protocol):
          def read(self) -> str:
              ...
      
      class AnyReader:  # No inheritance needed
          def read(self) -> str:
              return "content"
      
      def process(reader: ReaderProtocol) -> str:
          return reader.read()
      
      process(AnyReader())  # Works!
      process(FileReader())  # Also works!
      ```
      
      ## Common Protocols
      
      ### Supports Protocols
      
      ```python
      from typing import SupportsInt, SupportsFloat, SupportsBytes, SupportsAbs
      
      def to_int(value: SupportsInt) -> int:
          return int(value)
      
      to_int(3.14)   # OK - float supports __int__
      to_int("42")   # Error - str doesn't support __int__
      ```
      
      ### Iterator Protocol
      
      ```python
      from typing import Protocol, TypeVar
      
      T = TypeVar("T", covariant=True)
      
      class Iterator(Protocol[T]):
          def __next__(self) -> T:
              ...
      
      class Iterable(Protocol[T]):
          def __iter__(self) -> Iterator[T]:
              ...
      ```
      
      ### Context Manager Protocol
      
      ```python
      from typing import Protocol, TypeVar
      
      T = TypeVar("T")
      
      class ContextManager(Protocol[T]):
          def __enter__(self) -> T:
              ...
      
          def __exit__(
              self,
              exc_type: type[BaseException] | None,
              exc_val: BaseException | None,
              exc_tb: object | None,
          ) -> bool | None:
              ...
      ```
      
      ## Real-World Patterns
      
      ### Repository Pattern
      
      ```python
      from typing import Protocol, TypeVar
      
      T = TypeVar("T")
      
      class Repository(Protocol[T]):
          def get(self, id: int) -> T | None:
              ...
      
          def save(self, entity: T) -> None:
              ...
      
          def delete(self, id: int) -> bool:
              ...
      
      class User:
          id: int
          name: str
      
      class InMemoryUserRepo:
          def __init__(self):
              self._data: dict[int, User] = {}
      
          def get(self, id: int) -> User | None:
              return self._data.get(id)
      
          def save(self, entity: User) -> None:
              self._data[entity.id] = entity
      
          def delete(self, id: int) -> bool:
              return self._data.pop(id, None) is not None
      
      def process_users(repo: Repository[User]) -> None:
          user = repo.get(1)
          if user:
              repo.delete(user.id)
      ```
      
      ### Event Handler
      
      ```python
      from typing import Protocol
      
      class Event:
          pass
      
      class UserCreated(Event):
          def __init__(self, user_id: int):
              self.user_id = user_id
      
      class EventHandler(Protocol):
          def can_handle(self, event: Event) -> bool:
              ...
      
          def handle(self, event: Event) -> None:
              ...
      
      class UserCreatedHandler:
          def can_handle(self, event: Event) -> bool:
              return isinstance(event, UserCreated)
      
          def handle(self, event: Event) -> None:
              if isinstance(event, UserCreated):
                  print(f"User {event.user_id} created")
      
      def dispatch(event: Event, handlers: list[EventHandler]) -> None:
          for handler in handlers:
              if handler.can_handle(event):
                  handler.handle(event)
      ```
      
      ## Best Practices
      
      1. **Prefer Protocol over ABC** - For external interfaces
      2. **Use @runtime_checkable sparingly** - Has performance cost
      3. **Keep protocols minimal** - Single responsibility
      4. **Document expected behavior** - Protocols only define shape, not behavior
      5. **Combine protocols** - For complex requirements
      6. **Use Generic protocols** - For type-safe containers
      
    • runtime-validation.md 7.4 KB
      # Runtime Type Validation
      
      Enforce type hints at runtime with Pydantic, typeguard, and beartype.
      
      ## Choosing a Model Library
      
      | Library | Use When |
      |---------|----------|
      | **dataclasses** | Simple data containers, internal models, no validation needed |
      | **Pydantic** | API boundaries, user input, config, JSON serialization |
      | **attrs** | Performance-critical, many instances, custom validators |
      
      ```python
      # dataclasses - standard library, simple
      from dataclasses import dataclass
      
      @dataclass
      class Point:
          x: float
          y: float
      
      # Pydantic - validation + serialization
      from pydantic import BaseModel, Field
      
      class User(BaseModel):
          name: str = Field(min_length=1)
          email: EmailStr
      
      # attrs - fast, flexible
      import attrs
      
      @attrs.define
      class Record:
          id: int
          data: str = attrs.field(validator=attrs.validators.min_len(1))
      ```
      
      ## Pydantic v2 Validation
      
      ```python
      from pydantic import BaseModel, Field, field_validator, model_validator
      from pydantic import EmailStr, HttpUrl, PositiveInt
      from datetime import datetime
      from typing import Self
      
      class User(BaseModel):
          """Model with automatic validation."""
          id: PositiveInt
          name: str = Field(..., min_length=1, max_length=100)
          email: EmailStr
          website: HttpUrl | None = None
          created_at: datetime = Field(default_factory=datetime.now)
      
          @field_validator("name")
          @classmethod
          def name_must_be_title_case(cls, v: str) -> str:
              return v.title()
      
          @model_validator(mode="after")
          def check_consistency(self) -> Self:
              # Cross-field validation
              return self
      
      
      # Usage - raises ValidationError on invalid data
      user = User(id=1, name="john doe", email="john@example.com")
      print(user.name)  # "John Doe" (transformed)
      
      # From dict
      user = User.model_validate({"id": 1, "name": "jane", "email": "jane@example.com"})
      
      # Validation error
      try:
          User(id=-1, name="", email="invalid")
      except ValidationError as e:
          print(e.errors())
      ```
      
      ## Pydantic for Function Arguments
      
      ```python
      from pydantic import validate_call, Field
      from typing import Annotated
      
      @validate_call
      def greet(
          name: Annotated[str, Field(min_length=1)],
          count: Annotated[int, Field(ge=1, le=10)] = 1,
      ) -> str:
          return f"Hello, {name}!" * count
      
      
      # Valid
      greet("World")  # OK
      greet("World", count=3)  # OK
      
      # Invalid - raises ValidationError
      greet("")  # Error: min_length
      greet("World", count=100)  # Error: le
      ```
      
      ## typeguard (Runtime Type Checking)
      
      ```python
      from typeguard import typechecked, check_type
      from typing import TypeVar, Generic
      
      # Decorator for function checking
      @typechecked
      def process(items: list[int], multiplier: float) -> list[float]:
          return [item * multiplier for item in items]
      
      # Valid
      process([1, 2, 3], 1.5)  # OK
      
      # Invalid - raises TypeCheckError at runtime
      process(["a", "b"], 1.5)  # Error: list[int] expected
      
      
      # Check types manually
      from typeguard import check_type
      
      value = [1, 2, 3]
      check_type(value, list[int])  # OK
      
      value = [1, "two", 3]
      check_type(value, list[int])  # TypeCheckError
      
      
      # Class checking
      @typechecked
      class DataProcessor(Generic[T]):
          def __init__(self, data: list[T]):
              self.data = data
      
          def process(self) -> T:
              return self.data[0]
      ```
      
      ## beartype (Fast Runtime Checking)
      
      ```python
      from beartype import beartype
      from beartype.typing import List, Optional
      
      # ~200x faster than typeguard
      @beartype
      def fast_process(items: List[int], factor: float) -> List[float]:
          return [i * factor for i in items]
      
      
      # With optional
      @beartype
      def find_user(user_id: int) -> Optional[dict]:
          return None
      
      
      # Class decorator
      @beartype
      class FastProcessor:
          def __init__(self, data: list[int]):
              self.data = data
      
          def sum(self) -> int:
              return sum(self.data)
      ```
      
      ## TypedDict Runtime Validation
      
      ```python
      from typing import TypedDict, Required, NotRequired
      from pydantic import TypeAdapter
      
      class UserDict(TypedDict):
          id: Required[int]
          name: Required[str]
          email: NotRequired[str]
      
      
      # Using Pydantic to validate TypedDict
      adapter = TypeAdapter(UserDict)
      
      # Valid
      user = adapter.validate_python({"id": 1, "name": "John"})
      
      # Invalid - raises ValidationError
      adapter.validate_python({"id": "not-int", "name": "John"})
      
      
      # JSON parsing with validation
      user = adapter.validate_json('{"id": 1, "name": "John"}')
      ```
      
      ## dataclass Validation with Pydantic
      
      ```python
      from dataclasses import dataclass
      from pydantic import TypeAdapter
      from typing import Annotated
      from annotated_types import Gt, Lt
      
      @dataclass
      class Point:
          x: Annotated[float, Gt(-100), Lt(100)]
          y: Annotated[float, Gt(-100), Lt(100)]
      
      
      # Create validator
      validator = TypeAdapter(Point)
      
      # Validate
      point = validator.validate_python({"x": 10.5, "y": 20.3})
      
      # Or with init
      point = validator.validate_python(Point(x=10.5, y=20.3))
      ```
      
      ## Custom Validators
      
      ```python
      from pydantic import BaseModel, field_validator, ValidationInfo
      from pydantic_core import PydanticCustomError
      import re
      
      class Account(BaseModel):
          username: str
          password: str
      
          @field_validator("username")
          @classmethod
          def validate_username(cls, v: str) -> str:
              if not re.match(r"^[a-z][a-z0-9_]{2,19}$", v):
                  raise PydanticCustomError(
                      "invalid_username",
                      "Username must be 3-20 chars, start with letter, contain only a-z, 0-9, _"
                  )
              return v
      
          @field_validator("password")
          @classmethod
          def validate_password(cls, v: str, info: ValidationInfo) -> str:
              if len(v) < 8:
                  raise ValueError("Password must be at least 8 characters")
              if info.data.get("username") and info.data["username"] in v:
                  raise ValueError("Password cannot contain username")
              return v
      ```
      
      ## Constrained Types
      
      ```python
      from pydantic import (
          BaseModel,
          PositiveInt,
          NegativeFloat,
          conint,
          constr,
          conlist,
      )
      
      class Order(BaseModel):
          quantity: PositiveInt  # > 0
          discount: NegativeFloat | None = None  # < 0
      
          # Custom constraints
          product_code: constr(pattern=r"^[A-Z]{3}-\d{4}$")
          priority: conint(ge=1, le=5)
          tags: conlist(str, min_length=1, max_length=10)
      
      
      # Usage
      order = Order(
          quantity=5,
          product_code="ABC-1234",
          priority=3,
          tags=["urgent"]
      )
      ```
      
      ## When to Use Each
      
      | Tool | Speed | Strictness | Use Case |
      |------|-------|------------|----------|
      | Pydantic | Medium | High | API validation, config |
      | typeguard | Slow | Very high | Testing, debugging |
      | beartype | Fast | Medium | Production code |
      
      ```python
      # Development: Use typeguard for strictest checking
      from typeguard import typechecked
      
      @typechecked
      def dev_function(x: list[int]) -> int:
          return sum(x)
      
      
      # Production: Use beartype for minimal overhead
      from beartype import beartype
      
      @beartype
      def prod_function(x: list[int]) -> int:
          return sum(x)
      
      
      # API boundaries: Use Pydantic for validation + serialization
      from pydantic import BaseModel
      
      class Request(BaseModel):
          items: list[int]
      
      def api_function(request: Request) -> int:
          return sum(request.items)
      ```
      
      ## Quick Reference
      
      | Library | Decorator | Check |
      |---------|-----------|-------|
      | Pydantic | `@validate_call` | `Model.model_validate()` |
      | typeguard | `@typechecked` | `check_type(val, Type)` |
      | beartype | `@beartype` | Automatic on call |
      
      | Pydantic Type | Constraint |
      |---------------|------------|
      | `PositiveInt` | `> 0` |
      | `NegativeInt` | `< 0` |
      | `conint(ge=0, le=100)` | `0 <= x <= 100` |
      | `constr(min_length=1)` | Non-empty string |
      | `EmailStr` | Valid email |
      | `HttpUrl` | Valid URL |
      
    • type-narrowing.md 6 KB
      # Type Narrowing
      
      Techniques for narrowing types in conditional branches.
      
      ## isinstance Narrowing
      
      ```python
      def process(value: str | int | list[str]) -> str:
          if isinstance(value, str):
              # value is str here
              return value.upper()
          elif isinstance(value, int):
              # value is int here
              return str(value * 2)
          else:
              # value is list[str] here
              return ", ".join(value)
      ```
      
      ## None Checks
      
      ```python
      def greet(name: str | None) -> str:
          if name is None:
              return "Hello, stranger"
          # name is str here (not None)
          return f"Hello, {name}"
      
      # Also works with truthiness
      def greet_truthy(name: str | None) -> str:
          if name:
              # name is str here
              return f"Hello, {name}"
          return "Hello, stranger"
      ```
      
      ## Assertion Narrowing
      
      ```python
      def process(data: dict | None) -> str:
          assert data is not None
          # data is dict here
          return str(data.get("key"))
      
      def validate(value: int | str) -> int:
          assert isinstance(value, int), "Must be int"
          # value is int here
          return value * 2
      ```
      
      ## Type Guards
      
      ```python
      from typing import TypeGuard
      
      def is_string_list(val: list[object]) -> TypeGuard[list[str]]:
          """Check if all elements are strings."""
          return all(isinstance(x, str) for x in val)
      
      def process(items: list[object]) -> str:
          if is_string_list(items):
              # items is list[str] here
              return ", ".join(items)
          return "Not all strings"
      
      # With TypeVar
      from typing import TypeVar
      
      T = TypeVar("T")
      
      def is_not_none(val: T | None) -> TypeGuard[T]:
          return val is not None
      
      def process_optional(value: str | None) -> str:
          if is_not_none(value):
              # value is str here
              return value.upper()
          return "default"
      ```
      
      ## TypeIs (Python 3.13+)
      
      ```python
      from typing import TypeIs
      
      # TypeIs narrows more aggressively than TypeGuard
      def is_str(val: object) -> TypeIs[str]:
          return isinstance(val, str)
      
      def process(value: object) -> str:
          if is_str(value):
              # value is str here
              return value.upper()
          return "not a string"
      ```
      
      ## Discriminated Unions
      
      ```python
      from typing import Literal, TypedDict
      
      class SuccessResult(TypedDict):
          status: Literal["success"]
          data: dict
      
      class ErrorResult(TypedDict):
          status: Literal["error"]
          message: str
      
      Result = SuccessResult | ErrorResult
      
      def handle_result(result: Result) -> str:
          if result["status"] == "success":
              # result is SuccessResult
              return str(result["data"])
          else:
              # result is ErrorResult
              return f"Error: {result['message']}"
      ```
      
      ## Match Statement (Python 3.10+)
      
      ```python
      def describe(value: int | str | list[int]) -> str:
          match value:
              case int(n):
                  return f"Integer: {n}"
              case str(s):
                  return f"String: {s}"
              case [first, *rest]:
                  return f"List starting with {first}"
              case _:
                  return "Unknown"
      ```
      
      ## hasattr Narrowing
      
      ```python
      from typing import Protocol
      
      class HasName(Protocol):
          name: str
      
      def greet(obj: object) -> str:
          if hasattr(obj, "name") and isinstance(obj.name, str):
              # Type checkers may not narrow here
              # Use Protocol + isinstance instead
              return f"Hello, {obj.name}"
          return "Hello"
      ```
      
      ## Callable Narrowing
      
      ```python
      from collections.abc import Callable
      
      def execute(func_or_value: Callable[[], int] | int) -> int:
          if callable(func_or_value):
              # func_or_value is Callable[[], int]
              return func_or_value()
          else:
              # func_or_value is int
              return func_or_value
      ```
      
      ## Exhaustiveness Checking
      
      ```python
      from typing import Literal, Never
      
      def assert_never(value: Never) -> Never:
          raise AssertionError(f"Unexpected value: {value}")
      
      Status = Literal["pending", "active", "closed"]
      
      def handle_status(status: Status) -> str:
          if status == "pending":
              return "Waiting..."
          elif status == "active":
              return "In progress"
          elif status == "closed":
              return "Done"
          else:
              # If we add a new status, type checker will error here
              assert_never(status)
      ```
      
      ## Narrowing in Loops
      
      ```python
      from typing import TypeGuard
      
      def is_valid(item: str | None) -> TypeGuard[str]:
          return item is not None
      
      def process_items(items: list[str | None]) -> list[str]:
          result: list[str] = []
          for item in items:
              if is_valid(item):
                  # item is str here
                  result.append(item.upper())
          return result
      
      # Or use filter with type guard
      def process_items_functional(items: list[str | None]) -> list[str]:
          valid_items = filter(is_valid, items)
          return [item.upper() for item in valid_items]
      ```
      
      ## Class Type Narrowing
      
      ```python
      class Animal:
          pass
      
      class Dog(Animal):
          def bark(self) -> str:
              return "Woof!"
      
      class Cat(Animal):
          def meow(self) -> str:
              return "Meow!"
      
      def make_sound(animal: Animal) -> str:
          if isinstance(animal, Dog):
              return animal.bark()  # animal is Dog
          elif isinstance(animal, Cat):
              return animal.meow()  # animal is Cat
          return "..."
      ```
      
      ## Common Patterns
      
      ### Optional Unwrapping
      
      ```python
      def unwrap_or_default(value: T | None, default: T) -> T:
          if value is not None:
              return value
          return default
      
      # With early return
      def process(data: dict | None) -> dict:
          if data is None:
              return {}
          # data is dict for rest of function
          return {k: v.upper() for k, v in data.items()}
      ```
      
      ### Safe Dictionary Access
      
      ```python
      def get_nested(data: dict, *keys: str) -> object | None:
          result: object = data
          for key in keys:
              if not isinstance(result, dict):
                  return None
              result = result.get(key)
              if result is None:
                  return None
          return result
      ```
      
      ## Best Practices
      
      1. **Prefer isinstance** - Most reliable for type narrowing
      2. **Use TypeGuard** - For complex conditions
      3. **Check None explicitly** - `is None` or `is not None`
      4. **Use exhaustiveness checks** - Catch missing cases
      5. **Avoid hasattr** - Type checkers struggle with it
      6. **Match statements** - Clean pattern matching (3.10+)
      
  • scripts
    • check-types.sh 3.2 KB
      #!/bin/bash
      # Run type checkers with common options
      # Usage: ./check-types.sh [--mypy|--pyright|--both] [--strict] [path]
      
      set -e
      
      # Colors
      RED='\033[0;31m'
      GREEN='\033[0;32m'
      YELLOW='\033[1;33m'
      BLUE='\033[0;34m'
      NC='\033[0m'
      
      # Defaults
      CHECKER="both"
      STRICT=""
      TARGET="src"
      
      # Parse arguments
      while [[ $# -gt 0 ]]; do
          case $1 in
              --mypy)
                  CHECKER="mypy"
                  shift
                  ;;
              --pyright)
                  CHECKER="pyright"
                  shift
                  ;;
              --both)
                  CHECKER="both"
                  shift
                  ;;
              --strict)
                  STRICT="--strict"
                  shift
                  ;;
              *)
                  TARGET="$1"
                  shift
                  ;;
          esac
      done
      
      # Check if target exists
      if [[ ! -e "$TARGET" ]]; then
          echo -e "${RED}Target not found: $TARGET${NC}"
          exit 1
      fi
      
      run_mypy() {
          echo -e "${BLUE}=== Running mypy ===${NC}"
      
          if ! command -v mypy &> /dev/null; then
              echo -e "${YELLOW}mypy not found. Install with: uv add --dev mypy${NC}"
              return 1
          fi
      
          MYPY_ARGS="--show-error-codes --show-error-context --pretty"
          if [[ -n "$STRICT" ]]; then
              MYPY_ARGS="$MYPY_ARGS --strict"
          fi
      
          echo "mypy $MYPY_ARGS $TARGET"
          echo ""
      
          if mypy $MYPY_ARGS "$TARGET"; then
              echo -e "${GREEN}✓ mypy passed${NC}"
              return 0
          else
              echo -e "${RED}✗ mypy found errors${NC}"
              return 1
          fi
      }
      
      run_pyright() {
          echo -e "${BLUE}=== Running pyright ===${NC}"
      
          if ! command -v pyright &> /dev/null; then
              echo -e "${YELLOW}pyright not found. Install with: uv add --dev pyright${NC}"
              return 1
          fi
      
          PYRIGHT_ARGS=""
          if [[ -n "$STRICT" ]]; then
              # Create temporary config for strict mode
              TEMP_CONFIG=$(mktemp)
              cat > "$TEMP_CONFIG" << EOF
      {
        "typeCheckingMode": "strict"
      }
      EOF
              PYRIGHT_ARGS="--project $TEMP_CONFIG"
          fi
      
          echo "pyright $PYRIGHT_ARGS $TARGET"
          echo ""
      
          if pyright $PYRIGHT_ARGS "$TARGET"; then
              echo -e "${GREEN}✓ pyright passed${NC}"
              [[ -n "$STRICT" ]] && rm -f "$TEMP_CONFIG"
              return 0
          else
              echo -e "${RED}✗ pyright found errors${NC}"
              [[ -n "$STRICT" ]] && rm -f "$TEMP_CONFIG"
              return 1
          fi
      }
      
      # Run checkers
      MYPY_STATUS=0
      PYRIGHT_STATUS=0
      
      case $CHECKER in
          mypy)
              run_mypy || MYPY_STATUS=$?
              ;;
          pyright)
              run_pyright || PYRIGHT_STATUS=$?
              ;;
          both)
              run_mypy || MYPY_STATUS=$?
              echo ""
              run_pyright || PYRIGHT_STATUS=$?
              ;;
      esac
      
      # Summary
      echo ""
      echo -e "${BLUE}=== Summary ===${NC}"
      
      if [[ "$CHECKER" == "both" ]] || [[ "$CHECKER" == "mypy" ]]; then
          if [[ $MYPY_STATUS -eq 0 ]]; then
              echo -e "mypy:    ${GREEN}✓ passed${NC}"
          else
              echo -e "mypy:    ${RED}✗ failed${NC}"
          fi
      fi
      
      if [[ "$CHECKER" == "both" ]] || [[ "$CHECKER" == "pyright" ]]; then
          if [[ $PYRIGHT_STATUS -eq 0 ]]; then
              echo -e "pyright: ${GREEN}✓ passed${NC}"
          else
              echo -e "pyright: ${RED}✗ failed${NC}"
          fi
      fi
      
      # Exit with error if any checker failed
      if [[ $MYPY_STATUS -ne 0 ]] || [[ $PYRIGHT_STATUS -ne 0 ]]; then
          exit 1
      fi
      
  • SKILL.md 5.3 KB
    ---
    name: python-typing-ops
    description: "Python type hints and type safety patterns. Triggers on: type hints, typing, TypeVar, Generic, Protocol, mypy, pyright, type annotation, overload, TypedDict."
    license: MIT
    compatibility: "Python 3.10+ (uses union syntax X | Y). Some patterns require 3.11+ (Self, TypeVarTuple)."
    allowed-tools: "Read Write"
    metadata:
      author: claude-mods
      related-skills: python-pytest-ops
    ---
    
    # Python Typing Patterns
    
    Modern type hints for safe, documented Python code.
    
    ## Basic Annotations
    
    ```python
    # Variables
    name: str = "Alice"
    count: int = 42
    items: list[str] = ["a", "b"]
    mapping: dict[str, int] = {"key": 1}
    
    # Function signatures
    def greet(name: str, times: int = 1) -> str:
        return f"Hello, {name}!" * times
    
    # None handling
    def find(id: int) -> str | None:
        return db.get(id)  # May return None
    ```
    
    ## Collections
    
    ```python
    from collections.abc import Sequence, Mapping, Iterable
    
    # Use collection ABCs for flexibility
    def process(items: Sequence[str]) -> list[str]:
        """Accepts list, tuple, or any sequence."""
        return [item.upper() for item in items]
    
    def lookup(data: Mapping[str, int], key: str) -> int:
        """Accepts dict or any mapping."""
        return data.get(key, 0)
    
    # Nested types
    Matrix = list[list[float]]
    Config = dict[str, str | int | bool]
    ```
    
    ## Optional and Union
    
    ```python
    # Modern syntax (3.10+)
    def find(id: int) -> User | None:
        pass
    
    def parse(value: str | int | float) -> str:
        pass
    
    # With default None
    def fetch(url: str, timeout: float | None = None) -> bytes:
        pass
    ```
    
    ## TypedDict
    
    ```python
    from typing import TypedDict, Required, NotRequired
    
    class UserDict(TypedDict):
        id: int
        name: str
        email: str | None
    
    class ConfigDict(TypedDict, total=False):  # All optional
        debug: bool
        log_level: str
    
    class APIResponse(TypedDict):
        data: Required[list[dict]]
        error: NotRequired[str]
    
    def process_user(user: UserDict) -> str:
        return user["name"]  # Type-safe key access
    ```
    
    ## Callable
    
    ```python
    from collections.abc import Callable
    
    # Function type
    Handler = Callable[[str, int], bool]
    
    def register(callback: Callable[[str], None]) -> None:
        pass
    
    # With keyword args (use Protocol instead)
    from typing import Protocol
    
    class Processor(Protocol):
        def __call__(self, data: str, *, verbose: bool = False) -> int:
            ...
    ```
    
    ## Generics
    
    ```python
    from typing import TypeVar
    
    T = TypeVar("T")
    
    def first(items: list[T]) -> T | None:
        return items[0] if items else None
    
    # Bounded TypeVar
    from typing import SupportsFloat
    
    N = TypeVar("N", bound=SupportsFloat)
    
    def average(values: list[N]) -> float:
        return sum(float(v) for v in values) / len(values)
    ```
    
    ## Protocol (Structural Typing)
    
    ```python
    from typing import Protocol
    
    class Readable(Protocol):
        def read(self, n: int = -1) -> bytes:
            ...
    
    def load(source: Readable) -> dict:
        """Accepts any object with read() method."""
        data = source.read()
        return json.loads(data)
    
    # Works with file, BytesIO, custom classes
    load(open("data.json", "rb"))
    load(io.BytesIO(b"{}"))
    ```
    
    ## Type Guards
    
    ```python
    from typing import TypeGuard
    
    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]
            print(", ".join(items))
    ```
    
    ## Literal and Final
    
    ```python
    from typing import Literal, Final
    
    Mode = Literal["read", "write", "append"]
    
    def open_file(path: str, mode: Mode) -> None:
        pass
    
    # Constants
    MAX_SIZE: Final = 1024
    API_VERSION: Final[str] = "v2"
    ```
    
    ## Quick Reference
    
    | Type | Use Case |
    |------|----------|
    | `X \| None` | Optional value |
    | `list[T]` | Homogeneous list |
    | `dict[K, V]` | Dictionary |
    | `Callable[[Args], Ret]` | Function type |
    | `TypeVar("T")` | Generic parameter |
    | `Protocol` | Structural typing |
    | `TypedDict` | Dict with fixed keys |
    | `Literal["a", "b"]` | Specific values only |
    | `Final` | Cannot be reassigned |
    
    ## Type Checker Commands
    
    ```bash
    # mypy (run inside the project env)
    uv run mypy src/ --strict
    
    # pyright
    uv run pyright src/
    
    # In pyproject.toml
    [tool.mypy]
    strict = true
    python_version = "3.11"
    ```
    
    **Emerging: `ty`** — Astral's Rust-based type checker (same toolchain as uv +
    ruff), dramatically faster than mypy. Still in preview (pre-1.0), so mypy or
    pyright remain the production default — but worth watching, and easy to try:
    `uvx ty check`. Adopt for new projects once it stabilizes.
    
    ## Additional Resources
    
    - `./references/generics-advanced.md` - TypeVar, ParamSpec, TypeVarTuple
    - `./references/protocols-patterns.md` - Structural typing, runtime protocols
    - `./references/type-narrowing.md` - Guards, isinstance, assert
    - `./references/mypy-config.md` - mypy/pyright configuration
    - `./references/runtime-validation.md` - Pydantic v2, typeguard, beartype
    - `./references/overloads.md` - @overload decorator patterns
    
    ## Scripts
    
    - `./scripts/check-types.sh` - Run type checkers with common options
    
    ## Assets
    
    - `./assets/pyproject-typing.toml` - Recommended mypy/pyright config
    
    ---
    
    ## See Also
    
    This is a **foundation skill** with no prerequisites.
    
    **Related Skills:**
    - `python-pytest-ops` - Type-safe fixtures and mocking
    
    **Build on this skill:**
    - `python-async-ops` - Async type annotations
    - `python-fastapi-ops` - Pydantic models and validation
    - `python-database-ops` - SQLAlchemy type annotations
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related