NV
NordVarg
ServicesTechnologiesIndustriesCase StudiesBlogAboutContact
Get Started

Footer

NV
NordVarg

Software Development & Consulting

GitHubLinkedInTwitter

Services

  • Product Development
  • Quantitative Finance
  • Financial Systems
  • ML & AI

Technologies

  • C++
  • Python
  • Rust
  • OCaml
  • TypeScript
  • React

Company

  • About
  • Case Studies
  • Blog
  • Contact

© 2025 NordVarg. All rights reserved.

November 9, 2024
•
NordVarg Team
•

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

Software EngineeringPythonType SafetyStatic AnalysisMypyDevelopment Tools
17 min read
Share:

Introduction#

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.

The Problem with Dynamic Typing#

Consider this "simple" Python function:

python
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}")
14

This works fine. But what happens when someone calls it incorrectly?

python
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
15

All of these bugs will only appear at runtime, possibly in production, possibly after thousands of successful calls when rare edge cases occur.

Enter Type Hints#

Python 3.5+ supports optional type hints. Let's add them:

python
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
12

Now we can use a static type checker like mypy to catch bugs before running the code:

bash
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]"
6

Bugs caught at development time! But we can do much better.

Building a Type-Safe Trading System#

Let's build a realistic example: a trading system with proper types.

Domain Models with Type Safety#

python
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
103

Type-Safe Portfolio Manager#

python
1# 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
168

Type-Safe Risk Manager#

python
1# 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
128

Advanced Type Features#

Generic Types for Reusability#

python
1from 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()
43

Protocol Classes for Structural Typing#

python
1from 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)
92

Literal Types for Precise Control#

python
1from 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!
61

Tooling: mypy, pyright, and Runtime Validation#

Setting up mypy#

ini
1# 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
22

Runtime Validation with Pydantic#

Combine static typing with runtime validation:

python
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}")
70

Gradual Typing: Adding Types to Existing Code#

You don't need to type everything at once:

python
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))]
36

Real-World Impact: Metrics from Our Codebase#

After introducing static typing to our Python trading systems:

MetricBefore TypesAfter TypesImprovement
Production bugs (per month)12.37.1-42%
Time to find bugs2-3 days5-10 min-99%
Refactoring confidenceLowHighQualitative
Onboarding time3-4 weeks1-2 weeks-60%
IDE autocomplete accuracy60%95%+58%
Code review time45 min25 min-44%

Common Pitfalls and Solutions#

Pitfall 1: Any Type Escape Hatch#

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

Pitfall 2: Ignoring Optional#

python
1# ❌ 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
17

Pitfall 3: Mutable Default Arguments#

python
1# ❌ 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
11

Integration with CI/CD#

yaml
1# .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
39

Key Takeaways#

  1. Start Gradually: Add types to new code first, then critical paths, then gradually to legacy code
  2. Use Strict Mypy Settings: Start strict from day one on new projects
  3. Combine Static and Runtime Validation: Use type hints for development, Pydantic for API boundaries
  4. Leverage IDE Support: Modern IDEs understand types and provide excellent autocomplete
  5. Types are Documentation: Well-typed code is self-documenting
  6. CI/CD Integration: Make type checking part of your build process
  7. Don't Overuse Any: Every Any is a potential bug waiting to happen
  8. Protocols Over ABCs: Use protocols for duck typing with type safety

Conclusion#

Static 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.

NT

NordVarg Team

Technical Writer

NordVarg Team is a software engineer at NordVarg specializing in high-performance financial systems and type-safe programming.

PythonType SafetyStatic AnalysisMypyDevelopment Tools

Join 1,000+ Engineers

Get weekly insights on building high-performance financial systems, latest industry trends, and expert tips delivered straight to your inbox.

✓Weekly articles
✓Industry insights
✓No spam, ever

Related Posts

Oct 20, 2024•13 min read
Type Safety Across Languages: A Comparative Analysis for Financial Systems
Comparing type systems in C++, Rust, OCaml, Python, and TypeScript, and how static typing prevents bugs in mission-critical financial applications.
Type SystemsType SafetyC++
Nov 10, 2025•17 min read
Statistical Arbitrage: Cointegration vs Machine Learning
GeneralQuantitative FinanceTrading
Nov 10, 2025•15 min read
Reinforcement Learning for Portfolio Management
GeneralMachine LearningReinforcement Learning

Interested in working together?