Type Checking with mypy¶
Shaken Fist is incrementally adopting mypy for static type checking. This page documents the rollout strategy and guidelines for adding type annotations to the codebase.
Current Status¶
Type checking is enforced for:
shakenfist/schema/sqlalchemy.py- Pydantic to SQLAlchemy conversionshakenfist/schema/ipam_reservation.py- IPAM reservation Pydantic schemashakenfist/ipam.py- IPAM (IP Address Manager) class
Additional modules will be added over time as they are annotated.
Running mypy¶
To run type checking locally:
# Run mypy via tox (recommended)
tox -e mypy
# Or run mypy directly
mypy --strict --ignore-missing-imports shakenfist/schema/sqlalchemy.py
Configuration¶
mypy is configured in pyproject.toml:
[tool.mypy]
python_version = "3.11"
warn_return_any = true
warn_unused_configs = true
ignore_missing_imports = true
# Strict checking for typed modules
[[tool.mypy.overrides]]
module = "shakenfist.schema.sqlalchemy"
strict = true
[[tool.mypy.overrides]]
module = "shakenfist.schema.ipam_reservation"
strict = true
[[tool.mypy.overrides]]
module = "shakenfist.ipam"
strict = true
The ignore_missing_imports = true setting is necessary because some of our
dependencies (like shakenfist_utilities) don't have type stubs.
Rollout Strategy¶
We're adopting an incremental approach to type checking:
Phase 1: Schema Module (Current)¶
Start with the shakenfist/schema/ module because:
- Pydantic models already have type annotations
- The SQLAlchemy utilities benefit most from type checking
- It's a self-contained module with clear boundaries
Phase 2: New Code¶
All new code should include type annotations. When adding new files or significantly modifying existing ones, add type annotations and enable strict checking.
Phase 3: Core Utilities¶
Gradually add types to utility modules that are used across the codebase:
shakenfist/util/shakenfist/constants.pyshakenfist/config.py
Phase 4: Object Classes¶
Add types to the object model layer:
shakenfist/baseobject.pyshakenfist/instance.pyshakenfist/network/- etc.
Guidelines for Adding Types¶
Use Modern Syntax¶
Use Python 3.9+ type annotation syntax:
# Good - use built-in generics
def process(items: list[str]) -> dict[str, int]:
...
# Avoid - old typing module generics
def process(items: List[str]) -> Dict[str, int]:
...
Use Optional Explicitly¶
Never use implicit Optional. If a parameter can be None, annotate it explicitly:
# Good
def find(name: Optional[str] = None) -> Optional[Result]:
...
# Bad - implicit Optional
def find(name: str = None) -> Result: # mypy will reject this
...
Use Any Sparingly¶
When dealing with dynamic types, use Any but document why:
from typing import Any
def get_annotation(field: Any) -> Any:
"""Process a type annotation.
We use Any here because type annotations can be arbitrary types,
generic aliases, or special forms that don't have a common base.
"""
...
Leverage Pydantic¶
Pydantic models are already typed. Use them as the source of truth for types in related code:
from shakenfist.schema.operations.node_blob_op import model
def process_operation(op: model) -> None:
# op.uuid, op.blob_uuid, etc. are all properly typed
...
Adding a New Typed Module¶
- Add type annotations to all functions and classes
- Add an override section in
pyproject.toml:
- Add the module to the tox mypy command in
tox.ini - Run
tox -e mypyto verify
Common Patterns¶
Type Aliases¶
Define type aliases for complex types:
from typing import TypeAlias
IndexColumns: TypeAlias = tuple[str, ...]
IndexDefinition: TypeAlias = tuple[str, IndexColumns, bool]
Generic Classes¶
When writing generic utilities, use TypeVar:
from typing import TypeVar, Generic
T = TypeVar('T', bound='DatabaseBackedObject')
class Repository(Generic[T]):
def get(self, uuid: str) -> Optional[T]:
...
Callable Types¶
For callbacks and function parameters:
IDE Integration¶
Most modern IDEs support mypy integration:
- VS Code: Install the Pylance or mypy extensions
- PyCharm: Built-in support for type checking
- vim/neovim: Use ALE or coc.nvim with mypy
CI Integration¶
The mypy check runs as part of CI. Any PR that modifies a typed module must pass mypy strict checks.