Claude Agent Skill · by Wshobson

Python Type Safety

A comprehensive guide to Python's type system that goes beyond basic annotations. Covers generics, protocols, and type narrowing with practical patterns you'll

Install
Terminal · npx
$npx skills add https://github.com/wshobson/agents --skill python-type-safety
Works with Paperclip

How Python Type Safety fits into a Paperclip company.

Python Type Safety drops into any Paperclip agent that handles this kind of work. Assign it to a specialist inside a pre-configured PaperclipOrg company and the skill becomes available on every heartbeat — no prompt engineering, no tool wiring.

S
SaaS FactoryPaired

Pre-configured AI company — 18 agents, 18 skills, one-time purchase.

$27$59
Explore pack
Source file
SKILL.md432 lines
Expand
---name: python-type-safetydescription: Python type safety with type hints, generics, protocols, and strict type checking. Use when adding type annotations, implementing generic classes, defining structural interfaces, or configuring mypy/pyright.--- # Python Type Safety Leverage Python's type system to catch errors at static analysis time. Type annotations serve as enforced documentation that tooling validates automatically. ## When to Use This Skill - Adding type hints to existing code- Creating generic, reusable classes- Defining structural interfaces with protocols- Configuring mypy or pyright for strict checking- Understanding type narrowing and guards- Building type-safe APIs and libraries ## Core Concepts ### 1. Type Annotations Declare expected types for function parameters, return values, and variables. ### 2. Generics Write reusable code that preserves type information across different types. ### 3. Protocols Define structural interfaces without inheritance (duck typing with type safety). ### 4. Type Narrowing Use guards and conditionals to narrow types within code blocks. ## Quick Start ```pythondef get_user(user_id: str) -> User | None:    """Return type makes 'might not exist' explicit."""    ... # Type checker enforces handling None caseuser = get_user("123")if user is None:    raise UserNotFoundError("123")print(user.name)  # Type checker knows user is User here``` ## Fundamental Patterns ### Pattern 1: Annotate All Public Signatures Every public function, method, and class should have type annotations. ```pythondef get_user(user_id: str) -> User:    """Retrieve user by ID."""    ... def process_batch(    items: list[Item],    max_workers: int = 4,) -> BatchResult[ProcessedItem]:    """Process items concurrently."""    ... class UserRepository:    def __init__(self, db: Database) -> None:        self._db = db     async def find_by_id(self, user_id: str) -> User | None:        """Return User if found, None otherwise."""        ...     async def find_by_email(self, email: str) -> User | None:        ...     async def save(self, user: User) -> User:        """Save and return user with generated ID."""        ...``` Use `mypy --strict` or `pyright` in CI to catch type errors early. For existing projects, enable strict mode incrementally using per-module overrides. ### Pattern 2: Use Modern Union Syntax Python 3.10+ provides cleaner union syntax. ```python# Preferred (3.10+)def find_user(user_id: str) -> User | None:    ... def parse_value(v: str) -> int | float | str:    ... # Older style (still valid, needed for 3.9)from typing import Optional, Union def find_user(user_id: str) -> Optional[User]:    ...``` ### Pattern 3: Type Narrowing with Guards Use conditionals to narrow types for the type checker. ```pythondef process_user(user_id: str) -> UserData:    user = find_user(user_id)     if user is None:        raise UserNotFoundError(f"User {user_id} not found")     # Type checker knows user is User here, not User | None    return UserData(        name=user.name,        email=user.email,    ) def process_items(items: list[Item | None]) -> list[ProcessedItem]:    # Filter and narrow types    valid_items = [item for item in items if item is not None]    # valid_items is now list[Item]    return [process(item) for item in valid_items]``` ### Pattern 4: Generic Classes Create type-safe reusable containers. ```pythonfrom typing import TypeVar, Generic T = TypeVar("T")E = TypeVar("E", bound=Exception) class Result(Generic[T, E]):    """Represents either a success value or an error."""     def __init__(        self,        value: T | None = None,        error: E | None = None,    ) -> None:        if (value is None) == (error is None):            raise ValueError("Exactly one of value or error must be set")        self._value = value        self._error = error     @property    def is_success(self) -> bool:        return self._error is None     @property    def is_failure(self) -> bool:        return self._error is not None     def unwrap(self) -> T:        """Get value or raise the error."""        if self._error is not None:            raise self._error        return self._value  # type: ignore[return-value]     def unwrap_or(self, default: T) -> T:        """Get value or return default."""        if self._error is not None:            return default        return self._value  # type: ignore[return-value] # Usage preserves typesdef parse_config(path: str) -> Result[Config, ConfigError]:    try:        return Result(value=Config.from_file(path))    except ConfigError as e:        return Result(error=e) result = parse_config("config.yaml")if result.is_success:    config = result.unwrap()  # Type: Config``` ## Advanced Patterns ### Pattern 5: Generic Repository Create type-safe data access patterns. ```pythonfrom typing import TypeVar, Genericfrom abc import ABC, abstractmethod T = TypeVar("T")ID = TypeVar("ID") class Repository(ABC, Generic[T, ID]):    """Generic repository interface."""     @abstractmethod    async def get(self, id: ID) -> T | None:        """Get entity by ID."""        ...     @abstractmethod    async def save(self, entity: T) -> T:        """Save and return entity."""        ...     @abstractmethod    async def delete(self, id: ID) -> bool:        """Delete entity, return True if existed."""        ... class UserRepository(Repository[User, str]):    """Concrete repository for Users with string IDs."""     async def get(self, id: str) -> User | None:        row = await self._db.fetchrow(            "SELECT * FROM users WHERE id = $1", id        )        return User(**row) if row else None     async def save(self, entity: User) -> User:        ...     async def delete(self, id: str) -> bool:        ...``` ### Pattern 6: TypeVar with Bounds Restrict generic parameters to specific types. ```pythonfrom typing import TypeVarfrom pydantic import BaseModel ModelT = TypeVar("ModelT", bound=BaseModel) def validate_and_create(model_cls: type[ModelT], data: dict) -> ModelT:    """Create a validated Pydantic model from dict."""    return model_cls.model_validate(data) # Works with any BaseModel subclassclass User(BaseModel):    name: str    email: str user = validate_and_create(User, {"name": "Alice", "email": "a@b.com"})# user is typed as User # Type error: str is not a BaseModel subclassresult = validate_and_create(str, {"name": "Alice"})  # Error!``` ### Pattern 7: Protocols for Structural Typing Define interfaces without requiring inheritance. ```pythonfrom typing import Protocol, runtime_checkable @runtime_checkableclass Serializable(Protocol):    """Any class that can be serialized to/from dict."""     def to_dict(self) -> dict:        ...     @classmethod    def from_dict(cls, data: dict) -> "Serializable":        ... # User satisfies Serializable without inheriting from itclass User:    def __init__(self, id: str, name: str) -> None:        self.id = id        self.name = name     def to_dict(self) -> dict:        return {"id": self.id, "name": self.name}     @classmethod    def from_dict(cls, data: dict) -> "User":        return cls(id=data["id"], name=data["name"]) def serialize(obj: Serializable) -> str:    """Works with any Serializable object."""    return json.dumps(obj.to_dict()) # Works - User matches the protocolserialize(User("1", "Alice")) # Runtime checking with @runtime_checkableisinstance(User("1", "Alice"), Serializable)  # True``` ### Pattern 8: Common Protocol Patterns Define reusable structural interfaces. ```pythonfrom typing import Protocol class Closeable(Protocol):    """Resource that can be closed."""    def close(self) -> None: ... class AsyncCloseable(Protocol):    """Async resource that can be closed."""    async def close(self) -> None: ... class Readable(Protocol):    """Object that can be read from."""    def read(self, n: int = -1) -> bytes: ... class HasId(Protocol):    """Object with an ID property."""    @property    def id(self) -> str: ... class Comparable(Protocol):    """Object that supports comparison."""    def __lt__(self, other: "Comparable") -> bool: ...    def __le__(self, other: "Comparable") -> bool: ...``` ### Pattern 9: Type Aliases Create meaningful type names. **Note:** The `type Alias = ...` statement syntax (PEP 695) was introduced in **Python 3.12**, not 3.10. For projects targeting earlier versions (including 3.10/3.11), use the `TypeAlias` annotation (PEP 613, available since Python 3.10). ```python# Python 3.12+ type statement (PEP 695)type UserId = strtype UserDict = dict[str, Any] # Python 3.12+ type statement with generics (PEP 695)type Handler[T] = Callable[[Request], T]type AsyncHandler[T] = Callable[[Request], Awaitable[T]]``` ```python# Python 3.10-3.11 style (needed for broader compatibility)from typing import TypeAliasfrom collections.abc import Callable, Awaitable UserId: TypeAlias = strHandler: TypeAlias = Callable[[Request], Response]``` ```python# Usagedef register_handler(path: str, handler: Handler[Response]) -> None:    ...``` ### Pattern 10: Callable Types Type function parameters and callbacks. ```pythonfrom collections.abc import Callable, Awaitable # Sync callbackProgressCallback = Callable[[int, int], None]  # (current, total) # Async callbackAsyncHandler = Callable[[Request], Awaitable[Response]] # With named parameters (using Protocol)class OnProgress(Protocol):    def __call__(        self,        current: int,        total: int,        *,        message: str = "",    ) -> None: ... def process_items(    items: list[Item],    on_progress: ProgressCallback | None = None,) -> list[Result]:    for i, item in enumerate(items):        if on_progress:            on_progress(i, len(items))        ...``` ## Configuration ### Strict Mode Checklist For `mypy --strict` compliance: ```toml# pyproject.toml[tool.mypy]python_version = "3.12"strict = truewarn_return_any = truewarn_unused_ignores = truedisallow_untyped_defs = truedisallow_incomplete_defs = trueno_implicit_optional = true``` Incremental adoption goals:- All function parameters annotated- All return types annotated- Class attributes annotated- Minimize `Any` usage (acceptable for truly dynamic data)- Generic collections use type parameters (`list[str]` not `list`) For existing codebases, enable strict mode per-module using `# mypy: strict` or configure per-module overrides in `pyproject.toml`. ## Best Practices Summary 1. **Annotate all public APIs** - Functions, methods, class attributes2. **Use `T | None`** - Modern union syntax over `Optional[T]`3. **Run strict type checking** - `mypy --strict` in CI4. **Use generics** - Preserve type info in reusable code5. **Define protocols** - Structural typing for interfaces6. **Narrow types** - Use guards to help the type checker7. **Bound type vars** - Restrict generics to meaningful types8. **Create type aliases** - Meaningful names for complex types9. **Minimize `Any`** - Use specific types or generics. `Any` is acceptable for truly dynamic data or when interfacing with untyped third-party code10. **Document with types** - Types are enforceable documentation