Type Hinting Guide for RustyBT¶
Overview¶
RustyBT uses mypy with --strict mode for static type checking following PEP 484, PEP 526, and PEP 561. This guide explains our type hinting conventions, best practices, and gradual typing strategy.
Quick Reference¶
Basic Type Hints¶
from decimal import Decimal
from typing import Optional
def calculate_returns(
start_value: Decimal,
end_value: Decimal,
period_days: int
) -> Decimal:
"""Calculate annualized returns."""
daily_return = (end_value - start_value) / start_value
return daily_return * (Decimal(365) / Decimal(period_days))
Modern Python 3.12+ Syntax¶
RustyBT requires Python 3.12+, so use modern type hint syntax:
# ✅ Modern syntax (Python 3.12+)
def process_assets(assets: list[str]) -> dict[str, Decimal]:
...
def find_position(sid: int) -> Position | None:
...
# ❌ Old syntax (avoid)
from typing import List, Dict, Optional
def process_assets(assets: List[str]) -> Dict[str, Decimal]:
...
def find_position(sid: int) -> Optional[Position]:
...
Collection Types¶
# Lists
def get_sids() -> list[int]:
return [1, 2, 3]
# Dictionaries
def get_prices() -> dict[str, Decimal]:
return {"AAPL": Decimal("150.25")}
# Sets
def get_unique_assets() -> set[str]:
return {"AAPL", "GOOGL"}
# Tuples (fixed length)
def get_high_low() -> tuple[Decimal, Decimal]:
return (Decimal("100"), Decimal("90"))
Optional and Union Types¶
# Optional (value or None)
def get_position(sid: int) -> Position | None:
"""Returns position or None if not found."""
return self._positions.get(sid)
# Union (multiple possible types)
def process_price(value: Decimal | float) -> Decimal:
"""Accept either Decimal or float."""
if isinstance(value, float):
return Decimal(str(value))
return value
Generic Types¶
from typing import TypeVar, Generic
T = TypeVar('T')
class DataLoader(Generic[T]):
"""Generic data loader for any data type."""
def load(self, source: str) -> list[T]:
"""Load data from source."""
...
def validate(self, data: T) -> bool:
"""Validate a single data item."""
...
Protocols (Structural Typing)¶
from typing import Protocol
from decimal import Decimal
class BrokerAdapter(Protocol):
"""Protocol defining the broker adapter interface."""
def submit_order(self, symbol: str, quantity: Decimal) -> str:
"""Submit order and return order ID."""
...
def cancel_order(self, order_id: str) -> bool:
"""Cancel order by ID."""
...
# Any class implementing these methods satisfies the protocol
# No explicit inheritance required
Type Hinting Best Practices¶
1. Always Annotate Function Signatures¶
# ✅ Good: Complete type hints
def calculate_sharpe_ratio(
returns: list[Decimal],
risk_free_rate: Decimal
) -> Decimal:
...
# ❌ Bad: Missing annotations
def calculate_sharpe_ratio(returns, risk_free_rate):
...
2. Use None for Void Functions¶
# ✅ Good
def log_trade(order_id: str, price: Decimal) -> None:
logger.info("trade_executed", order_id=order_id, price=price)
# ❌ Bad: Missing return type
def log_trade(order_id: str, price: Decimal):
logger.info("trade_executed", order_id=order_id, price=price)
3. Avoid Any Unless Necessary¶
# ✅ Good: Specific types
def serialize_order(order: Order) -> dict[str, str | int | Decimal]:
...
# ⚠️ Acceptable with justification
from typing import Any
def serialize_to_json(obj: Any) -> str:
"""Serialize arbitrary object to JSON.
Uses Any because JSON supports arbitrary nested structures.
"""
return json.dumps(obj)
# ❌ Bad: Lazy use of Any
def process_data(data: Any) -> Any:
...
4. Type Narrow for Complex Logic¶
def process_price(value: str | Decimal | None) -> Decimal:
"""Process price from various input types."""
if value is None:
return Decimal(0)
if isinstance(value, str):
return Decimal(value)
# mypy knows value must be Decimal here
return value
5. Use TypeAlias for Complex Types¶
from typing import TypeAlias
# Define complex types once
PriceMap: TypeAlias = dict[str, dict[str, Decimal]]
PositionMap: TypeAlias = dict[int, Position]
def get_prices() -> PriceMap:
...
def get_positions() -> PositionMap:
...
Gradual Typing Strategy¶
RustyBT uses gradual typing to balance type safety with pragmatic development:
Strict Typing Modules¶
These modules enforce full mypy --strict compliance:
- ✅
rustybt.exceptions- Exception hierarchy - ✅
rustybt.utils.logging- Structured logging - ✅
rustybt.utils.error_handling- Error handling utilities - ✅ All new code (Epic 8+)
Gradual Migration Modules¶
Legacy Zipline modules have relaxed type checking (temporarily):
rustybt.algorithm- Core backtest algorithmrustybt.assets.*- Asset classesrustybt.data.*- Data infrastructurerustybt.finance.*- Finance calculationsrustybt.pipeline.*- Pipeline framework
Migration plan: These modules will be migrated to strict typing incrementally in future releases.
Configuration¶
Type checking behavior is configured in pyproject.toml:
[tool.mypy]
python_version = "3.12"
strict = true # Global strict mode
warn_return_any = true
# ... other strict flags
[[tool.mypy.overrides]]
module = [
"rustybt.algorithm",
"rustybt.assets.*",
# ... other legacy modules
]
# Temporarily disable strict checks for gradual migration
disallow_untyped_defs = false
disallow_untyped_calls = false
mypy Configuration¶
Running mypy Locally¶
# Check entire codebase
mypy rustybt/ --strict
# Check specific module
mypy rustybt/analytics/attribution.py --strict
# Check with coverage report
mypy rustybt/ --strict --any-exprs-report=.mypy_coverage
Pre-commit Integration¶
mypy runs automatically on staged files:
# Install pre-commit hooks
pre-commit install
# Run manually on all files
pre-commit run mypy --all-files
CI/CD Integration¶
GitHub Actions runs mypy on every PR:
Builds fail if mypy finds type errors in strict modules.
Common mypy Errors and Solutions¶
Error: Missing return type¶
# ❌ Error
def get_price(asset):
return asset.price
# ✅ Fix
def get_price(asset: Asset) -> Decimal:
return asset.price
Error: Incompatible return type¶
# ❌ Error (dict.get returns Optional[V])
def get_position(sid: int) -> Position:
return self._positions.get(sid)
# ✅ Fix (acknowledge None possibility)
def get_position(sid: int) -> Position | None:
return self._positions.get(sid)
Error: Untyped function call¶
# ❌ Error (calling function without type hints)
result = some_legacy_function(data)
# ✅ Fix (annotate the legacy function)
def some_legacy_function(data: DataFrame) -> ProcessedData:
...
# ⚠️ Temporary workaround (use with caution)
from typing import cast
result = cast(ProcessedData, some_legacy_function(data))
Error: Missing library stubs¶
# ❌ Error: "toolz" has no type stubs
import toolz
# ✅ Fix: Add module override in pyproject.toml
[[tool.mypy.overrides]]
module = ["toolz.*"]
ignore_missing_imports = true
Type Stubs for External Libraries¶
RustyBT includes type stubs for common libraries:
# pyproject.toml
dev = [
'mypy>=1.10.0',
'types-requests>=2.31.0',
'types-pytz>=2024.1.0',
'types-PyYAML>=6.0.12',
'pandas-stubs>=2.0.0',
'sqlalchemy-stubs>=0.4',
'types-python-dateutil>=2.8.19',
'types-networkx>=3.0',
'types-seaborn>=0.13.0',
]
Libraries without stubs (configured to ignore_missing_imports):
- toolz
- multipledispatch
- statsmodels
PEP 561 Typed Package¶
RustyBT is a PEP 561 typed package:
- Contains
rustybt/py.typedmarker file - Distributes
.pyistub files - Downstream projects can type-check against RustyBT APIs
Resources¶
- mypy Documentation
- PEP 484 - Type Hints
- PEP 526 - Variable Annotations
- PEP 561 - Distributing Type Information
- Python typing module
Migration Checklist for New Code¶
When adding new modules or functions:
- Add parameter type hints for all function arguments
- Add return type hints (use
-> Nonefor void functions) - Use Python 3.12+ syntax (
list[T],dict[K,V],T | None) - Avoid
Anyunless absolutely necessary (document why if used) - Run
mypy --stricton your module before committing - Verify pre-commit hook passes
- Ensure CI mypy check passes
Contact¶
For questions about type hinting in RustyBT:
- Review this guide
- Check pyproject.toml for current mypy configuration
- See examples in rustybt/exceptions.py, rustybt/utils/logging.py