Less boilerplate for classes that hold data
Many classes exist mainly to hold a few fields: a trade, a quote, a configuration. Writing __init__, __repr__ and __eq__ for each is repetitive. The @dataclass decorator from the dataclasses module writes them for you from a list of annotated fields. You declare symbol: str, price: float, quantity: int once, and you get a constructor Trade("AAPL", 184.25, 200), a readable repr like Trade(symbol='AAPL', price=184.25, quantity=200) and field-by-field equality. The annotations document intent; Python does not enforce them.
from dataclasses import dataclass
@dataclass
class Trade:
symbol: str
price: float
quantity: int
a = Trade("AAPL", 184.25, 200)
b = Trade("AAPL", 184.25, 200)
print(a) # Trade(symbol='AAPL', price=184.25, quantity=200)
print(a == b) # True: compared field by field
print(a.price * a.quantity) # 36850.0Defaults, field() and mutable values
A field can have a default (currency: str = "USD"), and fields with defaults must come after those without. For a mutable default such as a list, a plain = [] would be shared by every object (and Python refuses it); write trades: list = field(default_factory=list) so each object gets its own new list. field(...) also lets you hide a field from the repr (repr=False) or from comparisons (compare=False).
from dataclasses import dataclass, field
@dataclass
class Book:
name: str
currency: str = "USD" # a default
trades: list = field(default_factory=list) # a fresh list per object
one = Book("desk-a")
two = Book("desk-b")
one.trades.append(("AAPL", 100))
print(one) # Book(name='desk-a', currency='USD', trades=[('AAPL', 100)])
print(two.trades) # [] (not shared)
print(Book("x", "EUR")) # Book(name='x', currency='EUR', trades=[])Your own methods, __post_init__ and alternative constructors
A dataclass is still a normal class, so you can add methods. __post_init__(self) runs right after the generated __init__, which is the place to validate or clean values (self.symbol = self.symbol.upper(), raise ValueError(...) for a bad price). A @classmethod receives the class as cls and is the usual way to build an object from another form, such as text: Trade.from_line("AAPL,184.25,200") returns cls(symbol, float(price), int(quantity)), so the validation still runs and subclasses work too.
from dataclasses import dataclass
@dataclass
class Trade:
symbol: str
price: float
quantity: int
def __post_init__(self):
self.symbol = self.symbol.strip().upper()
if self.price <= 0:
raise ValueError("price must be positive")
def notional(self):
return self.price * abs(self.quantity)
@classmethod
def from_line(cls, line):
symbol, price, quantity = line.split(",")
return cls(symbol, float(price), int(quantity))
t = Trade.from_line(" aapl,184.25,-200")
print(t) # Trade(symbol='AAPL', price=184.25, quantity=-200)
print(t.notional()) # 36850.0Frozen, ordered, and when plain classes win
@dataclass(frozen=True) makes instances read-only (assigning to a field raises FrozenInstanceError) and gives them a hash, so they can be dictionary keys or set members: ideal for values that must not change, like a contract specification. @dataclass(order=True) adds <, <=, >, >= comparing the fields in order. Use a dataclass for records of data; for objects with a lot of behaviour and hidden state, a hand-written class with careful methods is clearer.
from dataclasses import dataclass
@dataclass(frozen=True, order=True)
class Contract:
symbol: str
expiry: str # ISO dates sort correctly as text
a = Contract("ESZ5", "2025-12-19")
b = Contract("ESZ5", "2026-03-20")
print(a < b) # True
print({a: "front", b: "next"}[a]) # front (frozen => hashable)
try:
a.symbol = "NQZ5"
except Exception as error:
print(type(error).__name__) # FrozenInstanceErrorprice: float do not stop Trade("AAPL", "abc", 1) from being created. If bad values would silently corrupt later calculations, check them in __post_init__ and raise ValueError, so a broken record cannot exist. Always create dataclass objects through the normal constructor, including from from_line, so the check cannot be skipped.@dataclassturns annotated fields into__init__,__repr__and__eq__.- Defaults come last; use
field(default_factory=list)for lists and dicts. __post_init__validates;@classmethodbuilds from other forms;frozen=Truemakes values read-only.
Next up: derivatives in code, estimating how fast a quantity changes, the idea behind option sensitivities such as delta and gamma.
Lab · what's graded
- Clean and validate a trade with
__post_init__25 pts - Compute the traded value with
notional25 pts - Build a trade from text with
from_line25 pts - Net the trades of one symbol with
net_quantity25 pts