python-mypy

SKILL.md

Python Mypy Type Checking Skill

This skill automatically activates when writing Python code to ensure proper type annotations and compatibility with Mypy static type checking.

Core Principles

  • Type Safety: Catch type errors before runtime
  • Gradual Typing: Start with critical paths, expand coverage over time
  • Strict Mode: Enable strict checks for new code
  • CI Integration: Run Mypy in continuous integration

Type Annotation Patterns

Function Signatures

from typing import Optional
from collections.abc import Sequence

# Good: Complete type hints
def process_items(
    items: list[str],
    max_count: int | None = None,
    debug: bool = False,
) -> dict[str, int]:
    """Process items and return counts."""
    result: dict[str, int] = {}
    # Implementation
    return result

# Good: Generic types with TypeVar
from typing import TypeVar

T = TypeVar('T')

def first(items: Sequence[T]) -> T | None:
    """Get first item from sequence."""
    return items[0] if items else None

Class Type Hints

from typing import ClassVar
from dataclasses import dataclass

@dataclass
class User:
    """User model with type hints."""

    id: int
    name: str
    email: str | None = None
    active: bool = True

    # Class variable
    _registry: ClassVar[dict[int, 'User']] = {}

    def __post_init__(self) -> None:
        """Register user after initialization."""
        self._registry[self.id] = self

Protocol for Structural Typing

from typing import Protocol

class Drawable(Protocol):
    """Protocol for drawable objects."""

    def draw(self) -> str:
        """Draw the object."""
        ...

def render(obj: Drawable) -> None:
    """Render any drawable object."""
    print(obj.draw())

# Any class with draw() method satisfies this
class Circle:
    def draw(self) -> str:
        return "○"

render(Circle())  # OK with Mypy

TypedDict for Structured Dictionaries

from typing import TypedDict, NotRequired

class UserDict(TypedDict):
    """Structured user dictionary."""
    id: int
    name: str
    email: NotRequired[str]  # Optional key (Python 3.11+)

def create_user(data: UserDict) -> None:
    """Create user from typed dictionary."""
    user_id: int = data["id"]  # Type-safe access
    # Mypy knows 'email' might not exist

Mypy Configuration Best Practices

Recommended mypy.ini

[mypy]
python_version = 3.11
warn_return_any = True
warn_unused_configs = True
disallow_untyped_defs = True
disallow_any_generics = True
disallow_subclassing_any = True
disallow_untyped_calls = True
disallow_incomplete_defs = True
check_untyped_defs = True
no_implicit_optional = True
warn_redundant_casts = True
warn_unused_ignores = True
warn_no_return = True
warn_unreachable = True
strict_equality = True
show_error_codes = True
show_column_numbers = True

# Start strict, relax per-module if needed
[mypy-tests.*]
disallow_untyped_defs = False

[mypy-migrations.*]
ignore_errors = True

# Third-party without stubs
[mypy-some_library.*]
ignore_missing_imports = True

pyproject.toml Configuration

[tool.mypy]
python_version = "3.11"
warn_return_any = true
warn_unused_configs = true
disallow_untyped_defs = true
show_error_codes = true

[[tool.mypy.overrides]]
module = "tests.*"
disallow_untyped_defs = false

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

Common Mypy Checks

Strict Optional Checking

# Bad: Implicit Optional
def find_user(id: int) -> User:  # Mypy error if can return None
    return users.get(id)  # dict.get returns User | None

# Good: Explicit Optional
def find_user(id: int) -> User | None:
    return users.get(id)

# Good: Narrow type with assertion
def get_user(id: int) -> User:
    user = users.get(id)
    assert user is not None, f"User {id} not found"
    return user  # Mypy knows this is User, not None

Type Narrowing

def process(value: str | int) -> str:
    """Process value based on type."""
    if isinstance(value, str):
        # Mypy knows value is str here
        return value.upper()
    else:
        # Mypy knows value is int here
        return str(value * 2)

Generics

from typing import Generic, TypeVar

T = TypeVar('T')

class Stack(Generic[T]):
    """Type-safe stack."""

    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()

# Usage
int_stack: Stack[int] = Stack()
int_stack.push(1)  # OK
int_stack.push("x")  # Mypy error!

Type Checking Strategies

Gradual Adoption

  1. Start with New Code: Use strict mode for new modules
  2. Core Paths First: Type-check critical business logic
  3. Expand Coverage: Gradually increase disallow_untyped_defs
  4. Per-Module Configuration: Use mypy overrides for legacy code
[mypy]
# Strict by default
disallow_untyped_defs = True

# Relax for legacy
[mypy-legacy.*]
disallow_untyped_defs = False
check_untyped_defs = True  # Still check what we can

Type Ignores (Use Sparingly)

# When third-party library lacks types
import untyped_library  # type: ignore[import-untyped]

# When dealing with dynamic code (rare)
def dynamic_call() -> Any:
    result = getattr(obj, method_name)()  # type: ignore[misc]
    return result

Common Patterns

Context Managers

from typing import Generator
from contextlib import contextmanager

@contextmanager
def database_transaction() -> Generator[Connection, None, None]:
    """Type-safe context manager."""
    conn = get_connection()
    try:
        yield conn
        conn.commit()
    except Exception:
        conn.rollback()
        raise
    finally:
        conn.close()

Callable Types

from collections.abc import Callable

def retry(
    func: Callable[[int], str],
    times: int = 3,
) -> str:
    """Retry a function that takes int and returns str."""
    for _ in range(times):
        try:
            return func(42)
        except Exception:
            continue
    raise RuntimeError("All retries failed")

Overloads for Multiple Signatures

from typing import overload

@overload
def parse(data: str) -> dict[str, str]: ...

@overload
def parse(data: bytes) -> dict[str, bytes]: ...

def parse(data: str | bytes) -> dict[str, str] | dict[str, bytes]:
    """Parse data with type-specific return."""
    if isinstance(data, str):
        return {"parsed": data}
    return {"parsed": data}

Django Integration

Model Type Hints

from django.db import models
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from django.db.models.manager import RelatedManager

class Author(models.Model):
    name = models.CharField(max_length=100)

    if TYPE_CHECKING:
        books: RelatedManager['Book']

class Book(models.Model):
    title = models.CharField(max_length=200)
    author: models.ForeignKey[Author] = models.ForeignKey(
        Author,
        on_delete=models.CASCADE,
        related_name='books',
    )

Django Mypy Plugin

[mypy]
plugins = mypy_django_plugin.main

[mypy.plugins.django-stubs]
django_settings_module = "myproject.settings"

Running Mypy

Basic Check

# Check all files
mypy .

# Check specific files/directories
mypy src/

# Show error codes
mypy --show-error-codes .

# Generate HTML report
mypy --html-report ./mypy-report .

In CI/CD

# .github/workflows/type-check.yml
- name: Type check with Mypy
  run: |
    pip install mypy
    mypy --strict src/

Anti-Patterns to Avoid

Don't Use Any Unnecessarily

# Bad: Any hides all type errors
def process(data: Any) -> Any:
    return data.unknown_method()  # No error!

# Good: Use specific types
def process(data: dict[str, int]) -> list[int]:
    return list(data.values())

Don't Ignore All Errors

# Bad: Blanket ignore
x = dangerous_call()  # type: ignore

# Good: Specific ignore with reason
x = legacy_api_call()  # type: ignore[misc]  # TODO: Add types to legacy API

Don't Mix str and bytes

# Bad: Mypy will catch this
def process(data: str) -> None:
    encoded: bytes = data  # Error!

# Good: Explicit conversion
def process(data: str) -> None:
    encoded: bytes = data.encode('utf-8')

Coverage Reporting

# Check type coverage
mypy --html-report ./coverage .

# Show coverage stats
mypy --any-exprs-report ./coverage .

Integration with Other Tools

  • Pre-commit: Run Mypy before commits
  • VS Code: Use Pylance with type checking mode
  • Ruff: Complement Mypy with Ruff for runtime checks

Related Skills

Skill Purpose
python-experts:python-style Python coding standards
python-experts:python-code-review Code review guidelines
python-experts:python-testing-expert Testing patterns

References

Weekly Installs
1
GitHub Stars
8
First Seen
6 days ago
Installed on
zencoder1
amp1
cline1
openclaw1
opencode1
cursor1