python-type-safety

SKILL.md

Python Type Safety

Implement comprehensive type safety in Python 3.10+ projects using static type checking with mypy and runtime validation with Pydantic.

When to Use This Skill

  • Adding type hints to existing Python code
  • Creating new typed modules and packages
  • Configuring mypy for strict type checking
  • Implementing Pydantic models for data validation
  • Migrating from untyped to fully typed code
  • Reviewing type coverage in a codebase

Type Safety Workflow

Phase 1: Assessment

  1. Check current type coverage

    # Install mypy if not present
    uv add mypy --dev
    
    # Run mypy to see current state
    uv run mypy src/ --ignore-missing-imports
    
  2. Identify priority areas

    • Public APIs (functions, classes exposed to users)
    • Data models and configuration
    • Core business logic
    • Integration boundaries

Phase 2: Implementation

  1. Add type hints systematically

    • Start with function signatures
    • Add class attribute types
    • Type variables and constants
    • Progress file by file
  2. Run mypy after each file

    uv run mypy src/module.py --strict
    
  3. Fix type errors incrementally

    • Address one error type at a time
    • Use # type: ignore[error-code] sparingly with comments

Phase 3: Validation

  1. Run full type check

    uv run mypy src/ --strict
    
  2. Verify runtime behavior unchanged

    uv run pytest
    

Type Hint Patterns

Function Signatures

from typing import Optional, Union
from collections.abc import Sequence, Mapping

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

# Optional parameters (can be None)
def find_user(user_id: int) -> Optional[User]:
    return db.get(user_id)  # Returns User or None

# Union types (multiple possible types)
def process(value: Union[str, int]) -> str:
    return str(value)

# Python 3.10+ union syntax
def process_modern(value: str | int | None) -> str:
    return str(value) if value else ""

# Collections with element types
def sum_values(numbers: Sequence[int]) -> int:
    return sum(numbers)

def merge_configs(configs: Mapping[str, str]) -> dict[str, str]:
    return dict(configs)

Class Attributes

from dataclasses import dataclass, field
from typing import ClassVar

@dataclass
class Module:
    """Infrastructure module with typed attributes."""

    # Instance attributes with types
    name: str
    version: str
    dependencies: list[str] = field(default_factory=list)
    enabled: bool = True

    # Optional attribute
    description: str | None = None

    # Class variable (shared across instances)
    registry: ClassVar[dict[str, "Module"]] = {}

    def __post_init__(self) -> None:
        """Register module after initialization."""
        Module.registry[self.name] = self

Generic Types

from typing import TypeVar, Generic
from collections.abc import Callable

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

class Result(Generic[T]):
    """Generic result container."""

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

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

    def map(self, fn: Callable[[T], K]) -> "Result[K]":
        if self._value is not None:
            return Result(fn(self._value))
        return Result(None, self._error)

Protocol (Structural Typing)

from typing import Protocol, runtime_checkable

@runtime_checkable
class Deployable(Protocol):
    """Protocol for deployable resources."""

    name: str

    def deploy(self, environment: str) -> bool:
        """Deploy to the specified environment."""
        ...

    def rollback(self) -> bool:
        """Rollback the deployment."""
        ...

# Any class with these methods/attributes satisfies the protocol
class Lambda:
    def __init__(self, name: str) -> None:
        self.name = name

    def deploy(self, environment: str) -> bool:
        return True

    def rollback(self) -> bool:
        return True

def deploy_resource(resource: Deployable) -> None:
    """Works with any Deployable, not just specific classes."""
    resource.deploy("production")

TypedDict for Structured Dictionaries

from typing import TypedDict, Required, NotRequired

class ModuleConfig(TypedDict):
    """Typed dictionary for module configuration."""

    name: Required[str]
    version: Required[str]
    enabled: NotRequired[bool]  # Optional key
    tags: NotRequired[list[str]]

def load_module(config: ModuleConfig) -> None:
    name = config["name"]  # Type: str
    enabled = config.get("enabled", True)  # Type: bool

Literal Types for Constants

from typing import Literal

Environment = Literal["development", "staging", "production"]
LogLevel = Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]

def deploy(env: Environment, log_level: LogLevel = "INFO") -> None:
    """Deploy with restricted environment values."""
    pass

# Type checker catches invalid values
deploy("production", "INFO")  # ✓ Valid
deploy("invalid", "INFO")     # ✗ Type error

Callable Types

from collections.abc import Callable, Awaitable

# Sync callback
Handler = Callable[[str, int], bool]

def process(handler: Handler) -> None:
    result = handler("test", 42)

# Async callback
AsyncHandler = Callable[[str], Awaitable[bool]]

async def process_async(handler: AsyncHandler) -> None:
    result = await handler("test")

# With *args and **kwargs
FlexibleHandler = Callable[..., None]

Pydantic Models

Basic Model

from pydantic import BaseModel, Field, field_validator

class ModuleSpec(BaseModel):
    """Module specification with validation."""

    name: str = Field(..., min_length=1, max_length=64)
    version: str = Field(..., pattern=r"^\d+\.\d+\.\d+$")
    dependencies: list[str] = Field(default_factory=list)
    enabled: bool = True

    @field_validator("name")
    @classmethod
    def name_must_be_lowercase(cls, v: str) -> str:
        if v != v.lower():
            raise ValueError("name must be lowercase")
        return v

Settings with Environment Variables

from pydantic_settings import BaseSettings, SettingsConfigDict

class AppSettings(BaseSettings):
    """Application settings from environment."""

    model_config = SettingsConfigDict(
        env_prefix="APP_",
        env_file=".env",
        env_file_encoding="utf-8",
    )

    database_url: str
    api_key: str = Field(..., min_length=32)
    debug: bool = False
    max_workers: int = Field(default=4, ge=1, le=32)

Nested Models

from pydantic import BaseModel
from datetime import datetime

class Author(BaseModel):
    name: str
    email: str

class Module(BaseModel):
    name: str
    version: str
    author: Author
    created_at: datetime
    dependencies: list["Module"] = []

# Automatic parsing from dict
data = {
    "name": "my-module",
    "version": "1.0.0",
    "author": {"name": "Dev", "email": "dev@example.com"},
    "created_at": "2024-01-15T10:30:00Z",
}
module = Module.model_validate(data)

Model with Custom Serialization

from pydantic import BaseModel, field_serializer, model_validator
from typing import Self

class DeploymentConfig(BaseModel):
    environment: str
    replicas: int
    memory_mb: int

    @model_validator(mode="after")
    def validate_resources(self) -> Self:
        if self.environment == "production" and self.replicas < 2:
            raise ValueError("Production requires at least 2 replicas")
        return self

    @field_serializer("memory_mb")
    def serialize_memory(self, value: int) -> str:
        return f"{value}MB"

mypy Configuration

Recommended pyproject.toml Settings

See mypy.toml for a complete configuration template.

[tool.mypy]
python_version = "3.11"
strict = true
warn_return_any = true
warn_unused_ignores = true
disallow_untyped_defs = true
disallow_incomplete_defs = true
check_untyped_defs = true
disallow_untyped_decorators = true
no_implicit_optional = true
warn_redundant_casts = true
warn_unused_configs = true
show_error_codes = true
show_column_numbers = true

# Per-module overrides for gradual adoption
[[tool.mypy.overrides]]
module = "tests.*"
disallow_untyped_defs = false

[[tool.mypy.overrides]]
module = "migrations.*"
ignore_errors = true

Gradual Adoption Strategy

# Phase 1: Basic (start here)
[tool.mypy]
python_version = "3.11"
warn_return_any = true
check_untyped_defs = true
show_error_codes = true

# Phase 2: Moderate
[tool.mypy]
python_version = "3.11"
warn_return_any = true
disallow_untyped_defs = true
check_untyped_defs = true
show_error_codes = true

# Phase 3: Strict (goal)
[tool.mypy]
python_version = "3.11"
strict = true
show_error_codes = true

Common Type Errors and Fixes

Error: Missing return type

# ✗ Error: Function is missing a return type annotation
def get_name(user):
    return user.name

# ✓ Fixed
def get_name(user: User) -> str:
    return user.name

Error: Incompatible types

# ✗ Error: Incompatible types in assignment
def process(value: str | None) -> str:
    result: str = value  # Error: can't assign str | None to str
    return result

# ✓ Fixed with guard
def process(value: str | None) -> str:
    if value is None:
        return ""
    return value  # Now known to be str

Error: Missing type for variable

# ✗ Error: Need type annotation for items
items = []
for x in data:
    items.append(x)

# ✓ Fixed with annotation
items: list[str] = []
for x in data:
    items.append(x)

Error: Optional access

# ✗ Error: Item "None" has no attribute "name"
def get_user_name(user: User | None) -> str:
    return user.name

# ✓ Fixed with assertion or guard
def get_user_name(user: User | None) -> str:
    if user is None:
        raise ValueError("User required")
    return user.name

Resources

Guidelines

  • Always use from __future__ import annotations for forward references
  • Prefer collections.abc types over typing equivalents (Python 3.9+)
  • Use X | Y union syntax over Union[X, Y] (Python 3.10+)
  • Add py.typed marker file for typed packages
  • Run mypy in CI/CD pipeline
  • Document complex type aliases with comments
  • Use typing.assert_type() for type debugging (Python 3.11+)
Weekly Installs
1
First Seen
12 days ago
Installed on
mcpjam1
claude-code1
junie1
windsurf1
zencoder1
crush1