The False Dichotomy
Walk into any developer discussion about OOP vs functional programming and you will find people arguing as if you have to choose one. You do not. Modern Python (and most practical languages) support both, and effective financial software uses whichever approach fits the problem at hand.
The real question is not "which is better?" but "when does each approach work best?"
Object-Oriented Programming: Modelling Entities
OOP excels when you are modelling things that have state and behaviour — accounts, orders, portfolios, instruments. The key concepts: classes, encapsulation, inheritance, and polymorphism.
class TradingAccount: def __init__(self, account_id: str, balance: float = 0.0): self.account_id = account_id self._balance = balance self._positions: dict[str, int] = {} self._trade_history: list[dict] = [] @property def balance(self) -> float: return self._balance def execute_trade(self, symbol: str, quantity: int, price: float, side: str): cost = quantity * price if side == "BUY": if cost > self._balance: raise InsufficientFundsError(f"Need {cost}, have {self._balance}") self._balance -= cost self._positions[symbol] = self._positions.get(symbol, 0) + quantity elif side == "SELL": current = self._positions.get(symbol, 0) if quantity > current: raise InsufficientPositionError(f"Have {current}, selling {quantity}") self._balance += cost self._positions[symbol] = current - quantity self._trade_history.append({ "symbol": symbol, "qty": quantity, "price": price, "side": side }) @property def portfolio_summary(self) -> dict: return dict(self._positions)
This is natural OOP: the TradingAccount encapsulates its state (balance, positions, history) and provides methods that enforce business rules (cannot sell what you do not have, cannot spend more than your balance). The private attributes (prefixed with _) signal that external code should not modify state directly.
When OOP Shines
- Stateful entities — accounts, sessions, connections, caches
- Complex business rules tied to specific entities
- Polymorphism — different instrument types sharing a common interface
- Framework integration — most web frameworks and ORMs expect classes
Functional Programming: Transforming Data
Functional programming treats computation as the evaluation of pure functions — given the same inputs, they always produce the same outputs, with no side effects. This makes code easier to test, reason about, and parallelise.
from functools import reduce from typing import NamedTuple class Trade(NamedTuple): symbol: str quantity: int price: float side: str def calculate_notional(trade: Trade) -> float: return trade.quantity * trade.price def is_buy(trade: Trade) -> bool: return trade.side == "BUY" def total_notional(trades: list[Trade]) -> float: return sum(map(calculate_notional, trades)) def filter_by_symbol(trades: list[Trade], symbol: str) -> list[Trade]: return [t for t in trades if t.symbol == symbol] def net_position(trades: list[Trade]) -> int: return reduce( lambda acc, t: acc + t.quantity if is_buy(t) else acc - t.quantity, trades, 0 ) # Each function is pure, testable, and composable trades = [ Trade("AAPL", 100, 150.0, "BUY"), Trade("AAPL", 50, 155.0, "SELL"), Trade("GOOGL", 25, 2800.0, "BUY"), ] aapl_trades = filter_by_symbol(trades, "AAPL") aapl_net = net_position(aapl_trades) # 50 aapl_notional = total_notional(aapl_trades) # 22750.0
No mutable state, no side effects, every function is independently testable. If net_position gives the wrong answer, you know the bug is in that function — it cannot be caused by hidden state elsewhere.
When FP Shines
- Data transformation pipelines — ETL, signal generation, analytics
- Calculations — pricing, risk, statistics (pure maths maps naturally to pure functions)
- Parallel processing — pure functions can run concurrently without synchronisation
- Testing — pure functions are trivially testable
The Practical Hybrid
The best Python codebases use both. Here is a common pattern in financial systems:
# OOP for entities and state management class Portfolio: def __init__(self): self.positions: dict[str, Position] = {} def apply_trade(self, trade: Trade): # State mutation is contained within the object if trade.symbol not in self.positions: self.positions[trade.symbol] = Position(trade.symbol) self.positions[trade.symbol].update(trade) # FP for calculations and transformations def calculate_portfolio_risk(positions: dict[str, Position], market_data: dict) -> float: """Pure function: same inputs always produce the same output.""" individual_risks = [ calculate_position_risk(pos, market_data) for pos in positions.values() ] correlations = get_correlation_matrix(list(positions.keys()), market_data) return aggregate_risk(individual_risks, correlations) def generate_signals(prices: pd.DataFrame, params: dict) -> pd.Series: """Pure function: no side effects, easily testable.""" fast_ma = prices.rolling(params["fast_window"]).mean() slow_ma = prices.rolling(params["slow_window"]).mean() return (fast_ma - slow_ma).apply(lambda x: 1 if x > 0 else -1)
The pattern: OOP for managing state and enforcing invariants, FP for computations and data transformations. The object holds the state; pure functions do the maths.
Immutability: The Key FP Principle to Adopt
Even if you use OOP extensively, adopting immutability where possible dramatically reduces bugs:
from dataclasses import dataclass @dataclass(frozen=True) class MarketQuote: symbol: str bid: float ask: float timestamp: float @property def mid(self) -> float: return (self.bid + self.ask) / 2 @property def spread(self) -> float: return self.ask - self.bid # Cannot be modified after creation quote = MarketQuote("AAPL", 150.00, 150.05, 1704067200.0) # quote.bid = 149.99 # This raises FrozenInstanceError
Frozen dataclasses are a practical way to get immutability in Python. They eliminate an entire category of bugs (unexpected mutation) and make concurrent code safer.
The Bottom Line
Do not be dogmatic. Use classes when you are modelling entities with state and behaviour. Use pure functions when you are transforming data or performing calculations. Use design patterns when they solve a real problem you have, not because they look clever.
The developers who are most effective in financial systems are pragmatists who reach for the right tool — OOP or FP — depending on the specific problem in front of them.
Want to go deeper on OOP vs Functional Programming: When to Use Which?
This article covers the essentials, but there's a lot more to learn. Inside Quantt, you'll find hands-on coding exercises, interactive quizzes, and structured lessons that take you from fundamentals to production-ready skills — across 50+ courses in technology, finance, and mathematics.
Free to get started · No credit card required