Coding Fundamentals
Python for finance automation — Decimal arithmetic, pandas, retry libraries, async I/O. DSA patterns hand-picked for the role: hashing, two-pointer, prefix sum. Big-O sanity for live-coding rounds.
Why Python is the lingua franca
Every piece of this role's stack expects Python:
- Anthropic SDK has a first-class Python client.
- MCP has a Python SDK.
- n8n integrates trivially with Python via HTTP / subprocess.
- Pandas is the analyst's lingua franca; Finance team members may even read your dataframes.
- Most finance vendor SDKs ship Python first.
If your live-coding round goes JavaScript or TypeScript, fine — but be ready to defend Python idioms in design discussions.
Money in Python — Decimal, not float
Never represent money as a float. Use decimal.Decimal. Float arithmetic produces silent errors that add up over a million-transaction close.
>>> 0.1 + 0.2
0.30000000000000004 # not 0.3
>>> from decimal import Decimal, getcontext, ROUND_HALF_EVEN
>>> getcontext().prec = 28
>>> getcontext().rounding = ROUND_HALF_EVEN
>>> Decimal("0.1") + Decimal("0.2")
Decimal('0.3')
Rules:
- Always construct
Decimalfrom a string, not from a float. - Pick rounding policy explicitly. ROUND_HALF_EVEN is the accounting default ("banker's rounding").
- Pick precision wide enough (28+) so you never lose digits during multi-step math.
- Persist money as a string in JSON / databases, or as a fixed-precision integer (cents) — never as a float.
Currency-aware money type
from decimal import Decimal
from dataclasses import dataclass
@dataclass(frozen=True)
class Money:
amount: Decimal
currency: str # ISO 4217: "USD", "EUR", "BTC"
def __add__(self, other: "Money") -> "Money":
if self.currency != other.currency:
raise ValueError(f"Cannot add {self.currency} and {other.currency}")
return Money(self.amount + other.amount, self.currency)
def __repr__(self):
return f"{self.amount} {self.currency}"
This eliminates whole classes of bugs at compile time. "Why can't I add EUR to USD?" is the right question for the type system to refuse.
Pandas patterns for finance
You'll use pandas constantly for reconciliation, variance, and exploratory work. Patterns that come up:
Joining two ledgers
import pandas as pd
from decimal import Decimal
gl = pd.read_csv("gl.csv", dtype={"amount": str})
bank = pd.read_csv("bank.csv", dtype={"amount": str})
gl["amount"] = gl["amount"].map(Decimal)
bank["amount"] = bank["amount"].map(Decimal)
# Inner join on (amount, date) for exact matches
matched = gl.merge(bank, on=["amount", "date"], how="inner",
suffixes=("_gl", "_bank"), indicator=False)
# Anti-joins for unmatched on either side
gl_only = gl.merge(bank, on=["amount", "date"], how="left", indicator=True) \
.query('_merge == "left_only"').drop(columns="_merge")
bank_only = bank.merge(gl, on=["amount", "date"], how="left", indicator=True) \
.query('_merge == "left_only"').drop(columns="_merge")
Period-over-period variance
tb = pd.read_csv("trial_balance_by_period.csv") # account, period, balance
pivot = tb.pivot(index="account", columns="period", values="balance")
pivot["variance"] = pivot["2026-04"] - pivot["2026-03"]
pivot["variance_pct"] = pivot["variance"] / pivot["2026-03"]
flag = pivot[pivot["variance"].abs() > Decimal("10000")]
Rolling windows for anomalies
# Daily expense by account; flag where today > 3 stdev above trailing 30 days
df["roll_mean"] = df.groupby("account")["amount"].transform(
lambda s: s.rolling(30, min_periods=10).mean())
df["roll_std"] = df.groupby("account")["amount"].transform(
lambda s: s.rolling(30, min_periods=10).std())
df["zscore"] = (df["amount"] - df["roll_mean"]) / df["roll_std"]
anomalies = df[df["zscore"].abs() > 3]
Robust API client patterns
Integrations to NetSuite / BlackLine / Kyriba / Fireblocks / Lukka share the same shape. Build a base client that handles auth, retries, idempotency, logging — then specialize.
import httpx, hashlib, time, logging
from tenacity import retry, stop_after_attempt, wait_exponential_jitter, \
retry_if_exception_type
log = logging.getLogger(__name__)
class FinanceAPIError(Exception): ...
class TransientError(FinanceAPIError): ...
class PermanentError(FinanceAPIError): ...
class BaseClient:
def __init__(self, base_url, auth, timeout=30):
self._c = httpx.AsyncClient(base_url=base_url, auth=auth, timeout=timeout)
@retry(
stop=stop_after_attempt(5),
wait=wait_exponential_jitter(initial=1, max=30),
retry=retry_if_exception_type(TransientError),
reraise=True,
)
async def request(self, method, path, *, idempotency_key=None, json=None):
headers = {}
if idempotency_key:
headers["Idempotency-Key"] = idempotency_key
r = await self._c.request(method, path, json=json, headers=headers)
if r.status_code == 429 or 500 <= r.status_code < 600:
raise TransientError(f"{r.status_code} {r.text[:200]}")
if r.status_code >= 400:
raise PermanentError(f"{r.status_code} {r.text[:200]}")
return r.json()
Note: permanent errors do not retry. A 422 (validation) won't get better on retry; raise and let the caller handle.
Retries with tenacity
The tenacity library is the standard retry primitive. The patterns to memorize:
- Exponential backoff with jitter — avoid thundering herd.
- Cap the wait — don't retry an hour later.
- Retry only specific exception classes — never blanket-catch.
- Reraise=True — so the last exception propagates.
- Stop after N — bounded; pair with a circuit breaker for unbounded systems.
Critical: when calling writes, the retry must carry the idempotency key — otherwise you double-write.
Async & concurrency
A close runs many recons in parallel. asyncio + httpx.AsyncClient is the default.
import asyncio
from contextlib import asynccontextmanager
async def reconcile_all(accounts, period):
sem = asyncio.Semaphore(8) # cap concurrency to avoid API rate limits
async def one(acct):
async with sem:
return acct, await reconcile_account(acct, period)
results = await asyncio.gather(*(one(a) for a in accounts),
return_exceptions=True)
return results
Common pitfalls to mention in interviews:
- Bound the concurrency with a semaphore; otherwise you'll get 429'd.
- return_exceptions=True so one failed recon doesn't kill the whole batch.
- Don't mix sync & async clients in the same call site.
- Logging context: use
contextvarsto carryrun_idacross the call tree.
DSA patterns hand-picked for this role
Live-coding rounds for this role tend to be applied finance algorithms rather than abstract leetcode. The patterns that recur:
| Pattern | When you reach for it |
|---|---|
| Hash table | Reconcile two ledgers; group by composite key; dedupe by idempotency key. |
| Two-pointer | Sorted time-series alignment; merging two sorted feeds. |
| Prefix sum | Running balance over a transaction stream; period totals from a daily series. |
| Sliding window | Rolling means, anomaly z-scores, treasury intraday monitoring. |
| Topological sort | Dependency-ordered execution of a close plan; resolving intercompany elimination order. |
| Bucketing / radix | Stratified sampling for SOX controls testing. |
| Counting / set ops | Set differences between source-system and GL sets. |
You don't need to memorize 200 leetcode problems. You need to recognize these patterns and reach for them naturally.
Big-O sanity
The level of analysis expected: name complexity out loud, justify in one sentence, propose an improvement if there's an obvious one.
- Reconciling two ledgers: naive O(n*m) double loop → O(n+m) with a hash table on the smaller side.
- Anomaly z-score over rolling 30 days: naive O(n*w) per account → O(n) with a streaming mean/variance.
- Sampling for SOX testing: reservoir sampling for one-pass O(n) on streaming.
- Topo sort of close plan: O(V+E) Kahn's algorithm.
If you can name the complexity and the corresponding pattern in two sentences, the live-coding round is mostly already won.
Testing finance code
Tests aren't a nice-to-have here; they are SOX evidence.
- Property-based testing (Hypothesis) for arithmetic: "for any list of transactions, sum(transactions) == final_balance - opening_balance."
- Golden-file tests for end-to-end recons: same inputs in → same outputs out.
- Fixture rebuilds: regenerate fixtures from production samples (PII-redacted) periodically.
- Mock the LLM with deterministic stubs for unit tests; reserve real-API calls for integration tests.
- Time control: never call
datetime.now()directly in business logic; inject a clock so tests are reproducible.
Live-coding shape — the senior moves
When given a problem in a 45-minute round:
- Restate the problem in your words.
- Clarify: input size, sorted?, currencies?, duplicates?, what if empty?
- Walk one example on a shared doc.
- Name the brute force and its complexity.
- Name the pattern ("two ledgers — I'd reach for a hash on the smaller side").
- Code with narration. Keep talking.
- Edge cases: empty, duplicate, single, very large, currency mismatch.
- Finance-aware tests: prove with a Decimal example and a duplicate example.
The very next chapter has 8 hand-picked problems with full solutions in drill mode.