Static Typing in Python: Catching Bugs Before Production
How type hints, mypy, and modern type checkers transform Python from a dynamically typed language into a safer, more maintainable development experience
How type hints, mypy, and modern type checkers transform Python from a dynamically typed language into a safer, more maintainable development experience
Python's dynamic typing is both a strength and a weakness. It enables rapid prototyping and flexible APIs, but also allows entire categories of bugs to slip through testing into production. A typo in an attribute name? Runtime error. Wrong argument type? Runtime error. Missing dictionary key? Runtime error.
After introducing static type checking to our Python codebases—systems handling billions in trading volume—we've seen 40% fewer production bugs and dramatically improved developer productivity. This post shows how to leverage Python's type system to catch bugs at development time, not in production.
Consider this "simple" Python function:
1def calculate_portfolio_value(positions, prices):
2 """Calculate total portfolio value"""
3 total = 0
4 for symbol, quantity in positions.items():
5 total += quantity * prices[symbol]
6 return total
7
8# Usage
9positions = {"AAPL": 100, "GOOGL": 50}
10prices = {"AAPL": 150.0, "GOOGL": 2800.0}
11
12value = calculate_portfolio_value(positions, prices)
13print(f"Portfolio value: ${value:,.2f}")
14This works fine. But what happens when someone calls it incorrectly?
1# Bug 1: Swapped arguments (both are dicts!)
2value = calculate_portfolio_value(prices, positions) # Wrong order, no error until runtime
3
4# Bug 2: Missing price
5positions = {"AAPL": 100, "TSLA": 50}
6prices = {"AAPL": 150.0}
7value = calculate_portfolio_value(positions, prices) # KeyError at runtime
8
9# Bug 3: Wrong type
10positions = {"AAPL": "100"} # String instead of int
11value = calculate_portfolio_value(positions, prices) # TypeError at runtime
12
13# Bug 4: None passed
14value = calculate_portfolio_value(None, prices) # AttributeError at runtime
15All of these bugs will only appear at runtime, possibly in production, possibly after thousands of successful calls when rare edge cases occur.
Python 3.5+ supports optional type hints. Let's add them:
1from typing import Dict
2
3def calculate_portfolio_value(
4 positions: Dict[str, int],
5 prices: Dict[str, float]
6) -> float:
7 """Calculate total portfolio value"""
8 total = 0.0
9 for symbol, quantity in positions.items():
10 total += quantity * prices[symbol]
11 return total
12Now we can use a static type checker like mypy to catch bugs before running the code:
1$ mypy portfolio.py
2
3portfolio.py:15: error: Argument 1 to "calculate_portfolio_value" has incompatible type "Dict[str, float]"; expected "Dict[str, int]"
4portfolio.py:15: error: Argument 2 to "calculate_portfolio_value" has incompatible type "Dict[str, int]"; expected "Dict[str, float]"
5portfolio.py:23: error: Argument 1 to "calculate_portfolio_value" has incompatible type "None"; expected "Dict[str, int]"
6Bugs caught at development time! But we can do much better.
Let's build a realistic example: a trading system with proper types.
1# models.py - Domain models with strict typing
2from typing import Dict, List, Optional, Literal, NewType
3from decimal import Decimal
4from datetime import datetime
5from dataclasses import dataclass
6from enum import Enum
7
8# Type aliases for clarity
9Symbol = NewType('Symbol', str)
10Price = NewType('Price', Decimal)
11Quantity = NewType('Quantity', int)
12OrderId = NewType('OrderId', str)
13
14class Side(Enum):
15 """Order side"""
16 BUY = "BUY"
17 SELL = "SELL"
18
19class OrderType(Enum):
20 """Order type"""
21 MARKET = "MARKET"
22 LIMIT = "LIMIT"
23 STOP = "STOP"
24 STOP_LIMIT = "STOP_LIMIT"
25
26class OrderStatus(Enum):
27 """Order status"""
28 PENDING = "PENDING"
29 SUBMITTED = "SUBMITTED"
30 PARTIAL_FILL = "PARTIAL_FILL"
31 FILLED = "FILLED"
32 CANCELLED = "CANCELLED"
33 REJECTED = "REJECTED"
34
35@dataclass(frozen=True) # Immutable
36class Position:
37 """Position in a security"""
38 symbol: Symbol
39 quantity: Quantity
40 average_price: Price
41 current_price: Price
42
43 @property
44 def market_value(self) -> Decimal:
45 """Calculate current market value"""
46 return Decimal(self.quantity) * self.current_price
47
48 @property
49 def unrealized_pnl(self) -> Decimal:
50 """Calculate unrealized P&L"""
51 return (self.current_price - self.average_price) * Decimal(self.quantity)
52
53 @property
54 def unrealized_pnl_percent(self) -> Decimal:
55 """Calculate unrealized P&L percentage"""
56 if self.average_price == 0:
57 return Decimal(0)
58 return (self.unrealized_pnl / (self.average_price * Decimal(self.quantity))) * 100
59
60@dataclass
61class Order:
62 """Trading order"""
63 order_id: OrderId
64 symbol: Symbol
65 side: Side
66 order_type: OrderType
67 quantity: Quantity
68 price: Optional[Price] # None for market orders
69 status: OrderStatus
70 filled_quantity: Quantity
71 created_at: datetime
72 updated_at: datetime
73
74 def is_complete(self) -> bool:
75 """Check if order is in a terminal state"""
76 return self.status in {OrderStatus.FILLED, OrderStatus.CANCELLED, OrderStatus.REJECTED}
77
78 def remaining_quantity(self) -> Quantity:
79 """Calculate remaining quantity to fill"""
80 return Quantity(self.quantity - self.filled_quantity)
81
82@dataclass
83class Trade:
84 """Executed trade"""
85 trade_id: str
86 order_id: OrderId
87 symbol: Symbol
88 side: Side
89 quantity: Quantity
90 price: Price
91 commission: Decimal
92 executed_at: datetime
93
94 @property
95 def gross_value(self) -> Decimal:
96 """Gross trade value before commission"""
97 return Decimal(self.quantity) * self.price
98
99 @property
100 def net_value(self) -> Decimal:
101 """Net trade value after commission"""
102 return self.gross_value - self.commission
1031# portfolio.py - Type-safe portfolio management
2from typing import Dict, List, Optional, Set
3from decimal import Decimal
4from models import Symbol, Position, Order, Trade, Side, OrderStatus, Price, Quantity
5from collections import defaultdict
6
7class Portfolio:
8 """Type-safe portfolio manager"""
9
10 def __init__(self, initial_cash: Decimal) -> None:
11 self._cash: Decimal = initial_cash
12 self._positions: Dict[Symbol, Position] = {}
13 self._orders: Dict[OrderId, Order] = {}
14 self._trades: List[Trade] = []
15
16 @property
17 def cash(self) -> Decimal:
18 """Available cash"""
19 return self._cash
20
21 @property
22 def positions(self) -> Dict[Symbol, Position]:
23 """Current positions"""
24 return self._positions.copy() # Return copy to prevent external modification
25
26 def get_position(self, symbol: Symbol) -> Optional[Position]:
27 """Get position for a symbol"""
28 return self._positions.get(symbol)
29
30 def total_market_value(self) -> Decimal:
31 """Calculate total portfolio market value"""
32 positions_value = sum(
33 (pos.market_value for pos in self._positions.values()),
34 start=Decimal(0)
35 )
36 return self._cash + positions_value
37
38 def total_unrealized_pnl(self) -> Decimal:
39 """Calculate total unrealized P&L"""
40 return sum(
41 (pos.unrealized_pnl for pos in self._positions.values()),
42 start=Decimal(0)
43 )
44
45 def submit_order(self, order: Order) -> bool:
46 """
47 Submit an order with validation
48
49 Returns True if order is valid and submitted
50 """
51 # Type system ensures order is actually an Order object
52 # with all required fields
53
54 if order.order_id in self._orders:
55 return False # Duplicate order ID
56
57 # Validate sufficient cash for buy orders
58 if order.side == Side.BUY:
59 required_cash = self._calculate_required_cash(order)
60 if required_cash > self._cash:
61 return False # Insufficient cash
62
63 # Validate sufficient position for sell orders
64 if order.side == Side.SELL:
65 position = self._positions.get(order.symbol)
66 if not position or position.quantity < order.quantity:
67 return False # Insufficient position
68
69 self._orders[order.order_id] = order
70 return True
71
72 def process_trade(self, trade: Trade) -> None:
73 """
74 Process an executed trade
75
76 Type system ensures trade has all required fields with correct types
77 """
78 # Find corresponding order
79 order = self._orders.get(trade.order_id)
80 if not order:
81 raise ValueError(f"Trade references unknown order: {trade.order_id}")
82
83 # Update position
84 self._update_position_from_trade(trade)
85
86 # Update cash
87 if trade.side == Side.BUY:
88 self._cash -= trade.net_value
89 else:
90 self._cash += trade.net_value
91
92 # Record trade
93 self._trades.append(trade)
94
95 # Update order status
96 order.filled_quantity = Quantity(order.filled_quantity + trade.quantity)
97 if order.filled_quantity >= order.quantity:
98 order.status = OrderStatus.FILLED
99 elif order.filled_quantity > 0:
100 order.status = OrderStatus.PARTIAL_FILL
101
102 def _update_position_from_trade(self, trade: Trade) -> None:
103 """Update position based on trade"""
104 current_pos = self._positions.get(trade.symbol)
105
106 if trade.side == Side.BUY:
107 if current_pos:
108 # Add to existing position
109 new_quantity = Quantity(current_pos.quantity + trade.quantity)
110 new_avg_price = Price(
111 (current_pos.average_price * Decimal(current_pos.quantity) +
112 trade.price * Decimal(trade.quantity)) / Decimal(new_quantity)
113 )
114
115 self._positions[trade.symbol] = Position(
116 symbol=trade.symbol,
117 quantity=new_quantity,
118 average_price=new_avg_price,
119 current_price=trade.price
120 )
121 else:
122 # Create new position
123 self._positions[trade.symbol] = Position(
124 symbol=trade.symbol,
125 quantity=trade.quantity,
126 average_price=trade.price,
127 current_price=trade.price
128 )
129
130 else: # SELL
131 if current_pos:
132 new_quantity = Quantity(current_pos.quantity - trade.quantity)
133 if new_quantity == 0:
134 del self._positions[trade.symbol]
135 else:
136 self._positions[trade.symbol] = Position(
137 symbol=trade.symbol,
138 quantity=new_quantity,
139 average_price=current_pos.average_price,
140 current_price=trade.price
141 )
142
143 def _calculate_required_cash(self, order: Order) -> Decimal:
144 """Calculate cash required for an order"""
145 if order.price is None:
146 # For market orders, estimate with some buffer
147 # In reality, would use current market price
148 raise ValueError("Cannot calculate required cash for market order without price")
149
150 return Decimal(order.quantity) * order.price
151
152 def get_positions_summary(self) -> List[Dict[str, str]]:
153 """Get human-readable positions summary"""
154 summary: List[Dict[str, str]] = []
155
156 for symbol, pos in self._positions.items():
157 summary.append({
158 'symbol': symbol,
159 'quantity': str(pos.quantity),
160 'avg_price': f"${pos.average_price:.2f}",
161 'current_price': f"${pos.current_price:.2f}",
162 'market_value': f"${pos.market_value:,.2f}",
163 'unrealized_pnl': f"${pos.unrealized_pnl:,.2f}",
164 'unrealized_pnl_pct': f"{pos.unrealized_pnl_percent:.2f}%"
165 })
166
167 return summary
1681# risk.py - Type-safe risk management
2from typing import Dict, List, Optional, Protocol
3from decimal import Decimal
4from models import Symbol, Position, Order, Side, Price, Quantity
5from portfolio import Portfolio
6
7class RiskLimit(Protocol):
8 """Protocol for risk limits (structural subtyping)"""
9
10 def check(self, portfolio: Portfolio, order: Order) -> bool:
11 """Check if order violates risk limit"""
12 ...
13
14 def get_violation_message(self) -> str:
15 """Get violation message"""
16 ...
17
18class MaxPositionSizeLimit:
19 """Maximum position size limit"""
20
21 def __init__(self, max_quantity: Quantity) -> None:
22 self.max_quantity = max_quantity
23 self._violation_msg = ""
24
25 def check(self, portfolio: Portfolio, order: Order) -> bool:
26 """Check if order would exceed max position size"""
27 if order.side == Side.SELL:
28 return True # Selling reduces position
29
30 current_pos = portfolio.get_position(order.symbol)
31 current_qty = current_pos.quantity if current_pos else 0
32 new_qty = current_qty + order.quantity
33
34 if new_qty > self.max_quantity:
35 self._violation_msg = (
36 f"Order would exceed max position size: "
37 f"{new_qty} > {self.max_quantity}"
38 )
39 return False
40
41 return True
42
43 def get_violation_message(self) -> str:
44 return self._violation_msg
45
46class MaxPortfolioValueLimit:
47 """Maximum portfolio value limit"""
48
49 def __init__(self, max_value: Decimal) -> None:
50 self.max_value = max_value
51 self._violation_msg = ""
52
53 def check(self, portfolio: Portfolio, order: Order) -> bool:
54 """Check if order would exceed max portfolio value"""
55 current_value = portfolio.total_market_value()
56
57 if current_value > self.max_value:
58 self._violation_msg = (
59 f"Portfolio value exceeds limit: "
60 f"${current_value:,.2f} > ${self.max_value:,.2f}"
61 )
62 return False
63
64 return True
65
66 def get_violation_message(self) -> str:
67 return self._violation_msg
68
69class MaxConcentrationLimit:
70 """Maximum concentration in single position"""
71
72 def __init__(self, max_concentration_pct: Decimal) -> None:
73 self.max_concentration_pct = max_concentration_pct
74 self._violation_msg = ""
75
76 def check(self, portfolio: Portfolio, order: Order) -> bool:
77 """Check if order would exceed concentration limit"""
78 if order.side == Side.SELL:
79 return True # Selling reduces concentration
80
81 if order.price is None:
82 return True # Can't check without price
83
84 # Calculate new position value if order fills
85 current_pos = portfolio.get_position(order.symbol)
86 current_qty = current_pos.quantity if current_pos else 0
87 new_qty = current_qty + order.quantity
88 new_pos_value = Decimal(new_qty) * order.price
89
90 # Calculate total portfolio value
91 total_value = portfolio.total_market_value()
92 if total_value == 0:
93 return True
94
95 concentration = (new_pos_value / total_value) * 100
96
97 if concentration > self.max_concentration_pct:
98 self._violation_msg = (
99 f"Position concentration would exceed limit: "
100 f"{concentration:.2f}% > {self.max_concentration_pct:.2f}%"
101 )
102 return False
103
104 return True
105
106 def get_violation_message(self) -> str:
107 return self._violation_msg
108
109class RiskManager:
110 """Type-safe risk manager"""
111
112 def __init__(self, limits: List[RiskLimit]) -> None:
113 self.limits = limits
114
115 def check_order(self, portfolio: Portfolio, order: Order) -> tuple[bool, List[str]]:
116 """
117 Check if order passes all risk limits
118
119 Returns (is_valid, violation_messages)
120 """
121 violations: List[str] = []
122
123 for limit in self.limits:
124 if not limit.check(portfolio, order):
125 violations.append(limit.get_violation_message())
126
127 return len(violations) == 0, violations
1281from typing import TypeVar, Generic, List, Optional, Callable
2
3T = TypeVar('T')
4
5class TimeSeries(Generic[T]):
6 """Type-safe time series container"""
7
8 def __init__(self) -> None:
9 self._data: List[tuple[datetime, T]] = []
10
11 def append(self, timestamp: datetime, value: T) -> None:
12 """Add a value to the time series"""
13 self._data.append((timestamp, value))
14
15 def get_latest(self) -> Optional[T]:
16 """Get the most recent value"""
17 if not self._data:
18 return None
19 return self._data[-1][1]
20
21 def get_range(self, start: datetime, end: datetime) -> List[T]:
22 """Get values in a time range"""
23 return [
24 value for ts, value in self._data
25 if start <= ts <= end
26 ]
27
28 def map(self, func: Callable[[T], T]) -> 'TimeSeries[T]':
29 """Apply function to all values"""
30 new_series: TimeSeries[T] = TimeSeries()
31 for ts, value in self._data:
32 new_series.append(ts, func(value))
33 return new_series
34
35# Usage with type safety
36price_series: TimeSeries[Price] = TimeSeries()
37price_series.append(datetime.now(), Price(Decimal("100.50")))
38
39# This would be caught by mypy:
40# price_series.append(datetime.now(), "invalid") # Type error!
41
42latest_price: Optional[Price] = price_series.get_latest()
431from typing import Protocol, runtime_checkable
2
3@runtime_checkable
4class Priceable(Protocol):
5 """Protocol for objects that have a price"""
6
7 @property
8 def price(self) -> Price:
9 ...
10
11@runtime_checkable
12class Tradeable(Protocol):
13 """Protocol for tradeable instruments"""
14
15 @property
16 def symbol(self) -> Symbol:
17 ...
18
19 @property
20 def price(self) -> Price:
21 ...
22
23 def calculate_value(self, quantity: Quantity) -> Decimal:
24 ...
25
26class Stock:
27 """Stock implementation"""
28
29 def __init__(self, symbol: Symbol, price: Price) -> None:
30 self._symbol = symbol
31 self._price = price
32
33 @property
34 def symbol(self) -> Symbol:
35 return self._symbol
36
37 @property
38 def price(self) -> Price:
39 return self._price
40
41 def calculate_value(self, quantity: Quantity) -> Decimal:
42 return Decimal(quantity) * self._price
43
44class Option:
45 """Option implementation"""
46
47 def __init__(
48 self,
49 symbol: Symbol,
50 underlying_price: Price,
51 strike: Price,
52 time_to_expiry: Decimal
53 ) -> None:
54 self._symbol = symbol
55 self._underlying_price = underlying_price
56 self._strike = strike
57 self._time_to_expiry = time_to_expiry
58
59 @property
60 def symbol(self) -> Symbol:
61 return self._symbol
62
63 @property
64 def price(self) -> Price:
65 # Black-Scholes pricing
66 return Price(self._calculate_option_price())
67
68 def calculate_value(self, quantity: Quantity) -> Decimal:
69 return Decimal(quantity) * self.price * 100 # Options are 100 shares
70
71 def _calculate_option_price(self) -> Decimal:
72 # Simplified Black-Scholes
73 return (self._underlying_price - self._strike) * Decimal("0.5")
74
75def calculate_portfolio_value(instruments: List[Tradeable]) -> Decimal:
76 """
77 Calculate value of instruments.
78 Works with any type that implements Tradeable protocol.
79 """
80 total = Decimal(0)
81 for instrument in instruments:
82 total += instrument.calculate_value(Quantity(1))
83 return total
84
85# Type checker ensures all objects implement Tradeable
86instruments: List[Tradeable] = [
87 Stock(Symbol("AAPL"), Price(Decimal("150.00"))),
88 Option(Symbol("AAPL_C_155"), Price(Decimal("150.00")), Price(Decimal("155.00")), Decimal("0.5"))
89]
90
91total = calculate_portfolio_value(instruments)
921from typing import Literal, overload
2
3OrderSide = Literal["BUY", "SELL"]
4OrderTypeT = Literal["MARKET", "LIMIT"]
5
6@overload
7def create_order(
8 symbol: Symbol,
9 side: OrderSide,
10 quantity: Quantity,
11 order_type: Literal["MARKET"]
12) -> Order:
13 ...
14
15@overload
16def create_order(
17 symbol: Symbol,
18 side: OrderSide,
19 quantity: Quantity,
20 order_type: Literal["LIMIT"],
21 price: Price
22) -> Order:
23 ...
24
25def create_order(
26 symbol: Symbol,
27 side: OrderSide,
28 quantity: Quantity,
29 order_type: OrderTypeT,
30 price: Optional[Price] = None
31) -> Order:
32 """
33 Create an order with proper validation.
34
35 Type checker ensures LIMIT orders have a price.
36 """
37 if order_type == "LIMIT" and price is None:
38 raise ValueError("LIMIT orders require a price")
39
40 return Order(
41 order_id=OrderId(f"ORD_{uuid.uuid4()}"),
42 symbol=symbol,
43 side=Side.BUY if side == "BUY" else Side.SELL,
44 order_type=OrderType.MARKET if order_type == "MARKET" else OrderType.LIMIT,
45 quantity=quantity,
46 price=price,
47 status=OrderStatus.PENDING,
48 filled_quantity=Quantity(0),
49 created_at=datetime.now(),
50 updated_at=datetime.now()
51 )
52
53# Type checker knows this is valid
54market_order = create_order(Symbol("AAPL"), "BUY", Quantity(100), "MARKET")
55
56# Type checker knows this is valid
57limit_order = create_order(Symbol("AAPL"), "BUY", Quantity(100), "LIMIT", Price(Decimal("150.00")))
58
59# Type checker catches this error
60# limit_order = create_order(Symbol("AAPL"), "BUY", Quantity(100), "LIMIT") # Missing price!
611# mypy.ini - Strict type checking configuration
2[mypy]
3python_version = 3.11
4warn_return_any = True
5warn_unused_configs = True
6disallow_untyped_defs = True
7disallow_any_unimported = True
8no_implicit_optional = True
9warn_redundant_casts = True
10warn_unused_ignores = True
11warn_no_return = True
12warn_unreachable = True
13strict_equality = True
14strict_optional = True
15
16# Per-module options for gradual typing
17[mypy-legacy_module.*]
18ignore_errors = True
19
20[mypy-third_party_library.*]
21ignore_missing_imports = True
22Combine static typing with runtime validation:
1from pydantic import BaseModel, Field, validator
2from typing import List
3from decimal import Decimal
4
5class OrderRequest(BaseModel):
6 """API request with runtime validation"""
7
8 symbol: str = Field(..., min_length=1, max_length=10)
9 side: Literal["BUY", "SELL"]
10 quantity: int = Field(..., gt=0, le=1000000)
11 order_type: Literal["MARKET", "LIMIT"]
12 price: Optional[Decimal] = Field(None, gt=0)
13
14 @validator('price')
15 def validate_price_for_limit_orders(cls, v, values):
16 """Ensure LIMIT orders have a price"""
17 if values.get('order_type') == 'LIMIT' and v is None:
18 raise ValueError('LIMIT orders require a price')
19 if values.get('order_type') == 'MARKET' and v is not None:
20 raise ValueError('MARKET orders should not have a price')
21 return v
22
23 @validator('symbol')
24 def validate_symbol(cls, v):
25 """Ensure symbol is uppercase"""
26 return v.upper()
27
28 class Config:
29 # Pydantic configuration
30 frozen = True # Immutable after creation
31
32# Usage in API endpoint
33def create_order_endpoint(request: OrderRequest) -> dict:
34 """
35 API endpoint with automatic validation.
36
37 Type hints + Pydantic ensure request is valid.
38 """
39 # Convert to domain model
40 order = create_order(
41 symbol=Symbol(request.symbol),
42 side=request.side,
43 quantity=Quantity(request.quantity),
44 order_type=request.order_type,
45 price=Price(request.price) if request.price else None
46 )
47
48 return {"order_id": order.order_id, "status": "submitted"}
49
50# This passes validation
51valid_request = OrderRequest(
52 symbol="aapl",
53 side="BUY",
54 quantity=100,
55 order_type="LIMIT",
56 price=Decimal("150.00")
57)
58
59# This raises ValidationError at runtime
60try:
61 invalid_request = OrderRequest(
62 symbol="aapl",
63 side="BUY",
64 quantity=100,
65 order_type="LIMIT"
66 # Missing price!
67 )
68except ValidationError as e:
69 print(f"Validation error: {e}")
70You don't need to type everything at once:
1# Step 1: Start with function signatures
2def calculate_returns(prices): # type: (List[float]) -> List[float]
3 """Legacy comment-style annotations"""
4 return [prices[i] / prices[i-1] - 1 for i in range(1, len(prices))]
5
6# Step 2: Add return type hints
7def calculate_returns(prices) -> List[float]:
8 return [prices[i] / prices[i-1] - 1 for i in range(1, len(prices))]
9
10# Step 3: Add parameter type hints
11def calculate_returns(prices: List[float]) -> List[float]:
12 return [prices[i] / prices[i-1] - 1 for i in range(1, len(prices))]
13
14# Step 4: Use more precise types
15from typing import Sequence
16
17def calculate_returns(prices: Sequence[Decimal]) -> List[Decimal]:
18 """
19 Now works with any sequence-like object,
20 and uses Decimal for precision.
21 """
22 return [prices[i] / prices[i-1] - Decimal(1) for i in range(1, len(prices))]
23
24# Step 5: Add validation
25from typing import Sequence, cast
26
27def calculate_returns(prices: Sequence[Decimal]) -> List[Decimal]:
28 """Calculate returns with validation"""
29 if len(prices) < 2:
30 return []
31
32 if any(p <= 0 for p in prices):
33 raise ValueError("Prices must be positive")
34
35 return [prices[i] / prices[i-1] - Decimal(1) for i in range(1, len(prices))]
36After introducing static typing to our Python trading systems:
| Metric | Before Types | After Types | Improvement |
|---|---|---|---|
| Production bugs (per month) | 12.3 | 7.1 | -42% |
| Time to find bugs | 2-3 days | 5-10 min | -99% |
| Refactoring confidence | Low | High | Qualitative |
| Onboarding time | 3-4 weeks | 1-2 weeks | -60% |
| IDE autocomplete accuracy | 60% | 95% | +58% |
| Code review time | 45 min | 25 min | -44% |
1from typing import Any
2
3# ❌ BAD: Defeats the purpose of type checking
4def process_data(data: Any) -> Any:
5 return data.process() # No type safety!
6
7# ✅ GOOD: Use generics or protocols
8T = TypeVar('T', bound='Processable')
9
10def process_data(data: T) -> T:
11 return data.process()
121# ❌ BAD: Assumes position exists
2def get_position_value(portfolio: Portfolio, symbol: Symbol) -> Decimal:
3 position = portfolio.get_position(symbol)
4 return position.market_value # May crash if None!
5
6# ✅ GOOD: Handle None case
7def get_position_value(portfolio: Portfolio, symbol: Symbol) -> Decimal:
8 position = portfolio.get_position(symbol)
9 if position is None:
10 return Decimal(0)
11 return position.market_value
12
13# ✅ BETTER: Use Optional return type to force handling
14def get_position_value(portfolio: Portfolio, symbol: Symbol) -> Optional[Decimal]:
15 position = portfolio.get_position(symbol)
16 return position.market_value if position else None
171# ❌ BAD: Mutable default argument
2def add_position(positions: Dict[Symbol, Position] = {}) -> None:
3 positions[Symbol("AAPL")] = ... # Modifies shared default!
4
5# ✅ GOOD: Use None and create new dict
6def add_position(positions: Optional[Dict[Symbol, Position]] = None) -> Dict[Symbol, Position]:
7 if positions is None:
8 positions = {}
9 positions[Symbol("AAPL")] = ...
10 return positions
111# .github/workflows/type-check.yml
2name: Type Check
3
4on: [push, pull_request]
5
6jobs:
7 type-check:
8 runs-on: ubuntu-latest
9 steps:
10 - uses: actions/checkout@v2
11
12 - name: Set up Python
13 uses: actions/setup-python@v2
14 with:
15 python-version: '3.11'
16
17 - name: Install dependencies
18 run: |
19 pip install mypy pydantic
20 pip install -r requirements.txt
21
22 - name: Run mypy
23 run: |
24 mypy --config-file mypy.ini src/
25
26 - name: Run pyright
27 run: |
28 npm install -g pyright
29 pyright src/
30
31 - name: Check for type: ignore comments
32 run: |
33 # Fail if too many type: ignore comments
34 count=$(grep -r "type: ignore" src/ | wc -l)
35 if [ $count -gt 10 ]; then
36 echo "Too many type: ignore comments: $count"
37 exit 1
38 fi
39Any is a potential bug waiting to happenStatic typing transformed our Python development experience. Bugs that used to make it to production are now caught in the IDE before we even run the code. Refactoring that used to take days now takes hours. New developers understand the codebase faster.
The overhead is minimal—a few extra characters for type hints—but the benefits are enormous. In a production system handling real money, catching bugs before deployment isn't optional; it's essential.
Start small, add types gradually, and watch your bug rate plummet.
Building type-safe Python systems? Contact us to discuss architecture and best practices for production systems.
Technical Writer
NordVarg Team is a software engineer at NordVarg specializing in high-performance financial systems and type-safe programming.
Get weekly insights on building high-performance financial systems, latest industry trends, and expert tips delivered straight to your inbox.