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
-
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 -
Identify priority areas
- Public APIs (functions, classes exposed to users)
- Data models and configuration
- Core business logic
- Integration boundaries
Phase 2: Implementation
-
Add type hints systematically
- Start with function signatures
- Add class attribute types
- Type variables and constants
- Progress file by file
-
Run mypy after each file
uv run mypy src/module.py --strict -
Fix type errors incrementally
- Address one error type at a time
- Use
# type: ignore[error-code]sparingly with comments
Phase 3: Validation
-
Run full type check
uv run mypy src/ --strict -
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 annotationsfor forward references - Prefer
collections.abctypes overtypingequivalents (Python 3.9+) - Use
X | Yunion syntax overUnion[X, Y](Python 3.10+) - Add
py.typedmarker 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
Repository
franciscosanche…factu-esFirst Seen
12 days ago
Security Audits
Installed on
mcpjam1
claude-code1
junie1
windsurf1
zencoder1
crush1