Data Structures, Functions & Modules
Professional tools for banking, fintech & algorithmic trading
Python's built-in data structures — dictionaries, sets, and tuples — are the backbone of professional financial systems. Combined with well-designed functions and Python's powerful standard library, they power everything from real-time fraud detection to algorithmic trade execution.
Every example in this chapter is drawn from banking, fintech, and trading. You'll build progressively — simple account lookups grow into portfolio risk engines; basic functions become fraud-scoring pipelines.
Dictionaries — Account Management
Dictionaries are Python's most powerful built-in structure — every customer record, account balance, and portfolio in a financial system is a dict. Keys map to values with O(1) average lookup.
Why dicts dominate in finance: O(1) lookup by account ID, flexible nested schema, dict comprehensions for filtering and aggregation.
Essential methods: .get(key, default) (safe lookup), .items(), .update(), .pop(key)
# Simple: safe account lookupaccounts = { "ACC001": {"name": "Alice Chen", "balance": 15_420.50, "tier": "silver"}, "ACC002": {"name": "Bob Patel", "balance": 3_800.00, "tier": "bronze"}, "ACC003": {"name": "Carol Singh", "balance": 92_100.75, "tier": "gold"},}print(accounts.get("ACC001", {}).get("balance")) # 15420.5print(accounts.get("ACC999")) # None — safe # Medium: comprehension + aggregationgold_only = {k: v for k, v in accounts.items() if v["tier"] == "gold"}total_aum = sum(v["balance"] for v in accounts.values())print(f"Gold : {list(gold_only.keys())}")print(f"AUM : £{total_aum:,.2f}") # Complex: nested update — assign credit limitslimits = {"gold": 50_000, "silver": 10_000, "bronze": 2_000}for acc_id, info in accounts.items(): info["credit_limit"] = limits[info["tier"]]for acc_id, info in accounts.items(): print(f"{acc_id} £{info['balance']:>10,.2f} credit £{info['credit_limit']:>6,}")Sets — Fraud Detection & Deduplication
Sets store unique elements with O(1) membership testing — ideal for blacklists, deduplication, and multi-source fraud correlation.
A & B— intersection (confirmed risk)A | B— union (all suspicious)A - B— difference (unique to one source)A ^ B— symmetric difference
Speed: x in blacklist is O(1) for a set vs O(n) for a list.
# Simple: blacklist screenblacklist = {"ACC099", "ACC042", "ACC007"}incoming_tx = {"ACC001", "ACC002", "ACC099", "ACC042"}flagged = incoming_tx & blacklistsafe = incoming_tx - blacklistprint(f"Flagged: {flagged} Safe: {safe}") # Medium: deduplicationraw_ids = ["TX1001","TX1002","TX1001","TX1003","TX1002","TX1004"]unique = set(raw_ids)print(f"Dupes removed: {len(raw_ids)-len(unique)}") # Complex: multi-bureau fraud correlationbureau_uk = {"ACC007", "ACC042", "ACC099", "ACC212"}bureau_eu = {"ACC042", "ACC099", "ACC200", "ACC315"}internal = {"ACC099", "ACC500"}confirmed_high = bureau_uk & bureau_eu & internalconfirmed_medium = ((bureau_uk & bureau_eu) | (bureau_uk & internal) | (bureau_eu & internal)) - confirmed_highprint(f"High risk : {confirmed_high}")print(f"Medium risk: {confirmed_medium}")Tuples & Immutability
Tuples are immutable sequences — once a trade executes, its record must never change. Immutability makes tuples ideal for financial records, audit logs, and fixed configs.
- Tuple unpacking —
symbol, price, qty = trade - Dict keys — tuples are hashable, so
(symbol, date)can index price history - namedtuple — adds field names, making records self-documenting
from collections import namedtuple # Simple: immutable trade recordtrade = ("AAPL", 182.50, 100, "BUY", "2024-01-15")symbol, price, qty, side, date = tradeprint(f"{side} {qty}x {symbol} @ £{price:.2f} — value £{price*qty:,.2f}")try: trade[1] = 999except TypeError as e: print(f"Cannot modify: {e}") # Medium: namedtuple for self-documenting recordsTrade = namedtuple("Trade", ["symbol", "price", "qty", "side", "date"])trades = [ Trade("TSLA", 210.00, 50, "SELL", "2024-01-15"), Trade("MSFT", 415.25, 75, "BUY", "2024-01-15"),]for t in sorted(trades, key=lambda t: t.price * t.qty, reverse=True): print(f"{t.side:4} {t.qty}x {t.symbol}: £{t.price * t.qty:,.2f}") # Complex: tuple as composite dict keyprice_history = {("AAPL","2024-01-14"): 180.25, ("AAPL","2024-01-15"): 182.50}prev, curr = price_history[("AAPL","2024-01-14")], price_history[("AAPL","2024-01-15")]print(f"AAPL day return: {(curr/prev - 1)*100:+.2f}%")Functions — Reusable Financial Logic
Functions are the building blocks of financial systems. Every business rule should live in a named, reusable, testable function.
Good function design: single responsibility, pure functions (same input → same output), return tuples for status/error pairs, docstrings documenting business rules.
# Simple: compound interestdef compound_interest(principal, annual_rate, years, n=12): """Returns (final_amount, interest_earned). n = periods/year.""" amount = principal * (1 + annual_rate / n) ** (n * years) return round(amount, 2), round(amount - principal, 2) final, earned = compound_interest(10_000, 0.065, 5)print(f"£10,000 @ 6.5% for 5yr → £{final:,.2f} (earned £{earned:,.2f})") # Medium: transfer validatordef validate_transfer(balance, amount, daily_limit, daily_spent=0): if amount <= 0: return False, "Amount must be positive" if amount > balance: return False, f"Insufficient funds (£{balance:,.2f})" remaining = daily_limit - daily_spent if amount > remaining: return False, f"Daily limit (£{remaining:,.2f} left)" return True, "Approved" for args in [(5000,1500,10000,0),(5000,6000,10000,0),(5000,1500,10000,9000)]: ok, msg = validate_transfer(*args) print(f" {'✓'if ok else '✗'} {msg}") # Complex: fraud risk scorerdef fraud_risk_score(amount, is_foreign, hour, past_flags=0): score = (30 if amount > 5000 else 15 if amount > 1000 else 0) score += (25 if is_foreign else 0) score += (20 if hour < 6 or hour > 22 else 0) score += min(past_flags * 10, 25) return min(score, 100) print(f"Normal daytime £500: {fraud_risk_score(500,False,14,0)}")print(f"Foreign £6k at 2am : {fraud_risk_score(6000,True,2,0)}")print(f"Repeat £2k : {fraud_risk_score(2000,False,11,3)}")*args & **kwargs — Flexible APIs
Financial APIs must handle variable inputs — a payment may have 1 or 10 metadata fields; a portfolio can hold any number of positions.
*args— extra positional args collected as a tuple**kwargs— extra keyword args collected as a dict
Real-world use: payment processor APIs, trade order builders, batch-processing pipelines.
# Simple: *args for variable portfolio positionsdef portfolio_value(*positions): """positions: (price, qty) pairs — any number.""" return round(sum(p * q for p, q in positions), 2) print(f"£{portfolio_value((182.50,100),(210.00,50),(415.25,75)):,.2f}") # Medium: **kwargs for transaction metadatadef create_transaction(acc_id, amount, tx_type, **metadata): return {"account": acc_id, "amount": amount, "type": tx_type, **metadata} tx = create_transaction("ACC001", 500.00, "transfer", reference="INV-001", channel="mobile")print(tx) # Complex: *args + **kwargs + defaultsdef process_batch(*account_ids, fee=0.0, notify=False, currency="GBP"): results = [{"account": a, "fee": fee, "currency": currency, "notified": notify} for a in account_ids] return results, round(fee * len(account_ids), 2) records, total = process_batch("ACC001","ACC002","ACC003", fee=2.50, notify=True)print(f"Processed {len(records)}, total fees: £{total:.2f}")for r in records: print(f" {r}")Lambda & Higher-order Functions
Lambdas are anonymous single-expression functions — used as sort keys, filter criteria, and data transformations on financial datasets.
sorted(data, key=lambda x: ...)— rank positions by P&L or exposurefilter(lambda x: ..., data)— screen transactions or flag losersmap(lambda x: ..., data)— apply tax rates or transformations
from collections import namedtuple Trade = namedtuple("Trade", ["symbol", "buy", "now", "qty"])portfolio = [ Trade("AAPL", 150.00, 182.50, 100), Trade("TSLA", 250.00, 210.00, 50), Trade("MSFT", 380.00, 415.25, 75), Trade("NVDA", 90.00, 485.00, 30), Trade("META", 120.00, 374.00, 60),] # Simple: sort by P&Lpnl = lambda t: (t.now - t.buy) * t.qtyfor t in sorted(portfolio, key=pnl, reverse=True): p = pnl(t) print(f"{t.symbol:5} {'+'if p>=0 else ''}£{p:>9,.2f}") # Medium: filter gainers → map to CGTgainers = list(filter(lambda t: pnl(t) > 0, portfolio))cgt = list(map(lambda t: (t.symbol, round(pnl(t)*0.20, 2)), gainers))print("\nCGT (20%):", cgt) # Complex: multi-key risk screenexposure = lambda t: t.now * t.qtypct_rtn = lambda t: (t.now - t.buy) / t.buyranked = sorted(portfolio, key=lambda t: (-exposure(t), pct_rtn(t)))print("\nRisk screen (exposure ↓, return ↑):")for t in ranked: print(f" {t.symbol:5} £{exposure(t):>9,.2f} {pct_rtn(t)*100:>+6.1f}%")Modules & the Standard Library
Python's standard library ships battle-tested modules critical for financial engineering:
decimal— exact base-10 arithmetic (0.1 + 0.2in float =0.30000000000000004)datetime— trade timestamps, T+2 settlement, period calculationscollections—Counter(transaction frequency),defaultdict,namedtuplemath— log returns, volatility, option pricing formulas
from decimal import Decimal, ROUND_HALF_UPfrom datetime import date, timedeltafrom collections import Counterimport math # Decimal: exact moneyvalue = Decimal("182.50") * Decimal("100")fee = (value * Decimal("0.001")).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)print(f"Trade: £{value} Fee: £{fee} Net: £{value-fee}")print(f"Float trap : 0.1+0.2 = {0.1+0.2}")print(f"Decimal fix: {Decimal('0.1')+Decimal('0.2')}") # datetime: T+2 settlementtrade_date = date(2024, 1, 15)print(f"\nSettlement: {trade_date + timedelta(days=2)}") # Counter: transaction frequencytx_log = ["purchase","transfer","purchase","atm","purchase","atm","refund"]print(f"Tx mix: {Counter(tx_log).most_common()}") # math: log returns & daily volatilityprices = [100,102,101,105,103,108,112]log_rets = [math.log(prices[i]/prices[i-1]) for i in range(1,len(prices))]mean_r = sum(log_rets)/len(log_rets)vol = math.sqrt(sum((r-mean_r)**2 for r in log_rets)/len(log_rets))print(f"Daily vol: {vol*100:.3f}%")Practice Questions
Question 1
What does accounts.get('ACC999') return if 'ACC999' is not in the dict?
Question 2
Which set operation returns accounts that appear in BOTH fraud bureau A AND bureau B?
Question 3
Why can tuples be used as dictionary keys but lists cannot?
Question 4
Given Trade = namedtuple('Trade', ['symbol','price']) and t = Trade('AAPL', 182.50), which access methods are valid?
Question 5
Inside def process(*amounts), what Python type is 'amounts'?
Question 6
What does sorted(portfolio, key=lambda t: t.price * t.qty, reverse=True) sort by?
Question 7
Why use Decimal('0.1') + Decimal('0.2') instead of 0.1 + 0.2 in financial code?
Question 8
What does list(filter(lambda t: pnl(t) > 0, portfolio)) return?