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.
✓ Check yourself
Answer from memory before opening each one.
-
A colleague writes Decimal(1.1) + Decimal(2.2) and is surprised the total is not exactly 3.3. What happened, and what is the rule?
Show answer
The floats were already inexact before Decimal saw them, so Decimal faithfully stored the error. Build Decimal from strings, such as Decimal("1.1"), and store money as strings or integer minor units. Set the rounding policy explicitly. See money in Python.
-
Why do the pandas examples read amount columns as strings and convert them, instead of letting the reader infer a numeric type?
Show answer
The default reader parses amounts as binary floats, which loses exactness before any arithmetic starts. Reading text and mapping each value to Decimal preserves the digits, so a join on amount compares exact values. See pandas patterns.
-
An API returns 422 with a validation message and your retry wrapper retries every HTTP error five times. What is wrong, and what should the wrapper treat as retryable?
Show answer
A 422 will not improve on retry, so the wrapper wastes time and, on writes, risks repeat effects. Retry only transient conditions: timeouts, rate limits and 5xx. Raise permanent errors immediately, and on writes pass the same idempotency key each attempt. See the API client and retries.
-
You reconcile 200 accounts with asyncio.gather and no semaphore, and one account raises. What goes wrong?
Show answer
Launching all 200 at once triggers rate limits, so you create the failures you are guarding against. Without return_exceptions, the first exception propagates and you lose the batch's results. Cap concurrency with a semaphore and collect exceptions per account. See async and concurrency.
-
Two ledgers of two million lines each must be matched on amount, date and reference. What is the naive cost, what do you use, and what changes if identical lines can repeat?
Show answer
A double loop is O(n × m); a hash table on one side brings it to O(n + m). With repeats, a plain set loses multiplicity, so map each key to a count or a list and consume one entry per match. See DSA patterns and Big-O.
-
A unit test for month-end variance passes all month and fails on the last day of each month. What is the likely cause, and what is the fix?
Show answer
The business logic reads the current date directly, so the test depends on when it runs. Inject a clock into the code under test, and pass a fixed date in tests. Reproducible runs matter here because tests are part of the evidence for the control. See testing finance code.
Takeaway
- Money is Decimal built from strings with an explicit rounding policy, and it is stored as text or integer minor units. A Money type that refuses mixed currencies removes a class of bugs.
- In pandas, read amounts as strings, convert to Decimal, and use joins and anti-joins to split matched from unmatched.
- A shared base client separates transient from permanent errors, retries only the first with jittered, capped backoff, and sends the idempotency key on every write.
- Run concurrent work with bounded concurrency, collect failures per item instead of failing the batch, and carry the run identifier through logs.
- The recurring patterns are hashing, two pointers, prefix sums, sliding windows and topological sort. Name the pattern and the complexity aloud.
- Tests are control evidence: property tests for arithmetic, golden files for recons, a mocked model in unit tests and an injected clock.
With the toolkit set, the next chapter applies it to ten finance-flavoured problems to drill. → Coding Problems
Further reading
- decimal: Decimal fixed point and floating point arithmetic — Python documentation. Contexts, rounding modes and construction from strings.
- Merge, join, concatenate and compare — pandas documentation. Joins and the indicator column used for anti-joins.
- Tenacity — Tenacity documentation. Stop, wait and retry conditions.
- Coroutines and Tasks — Python documentation. How gather handles exceptions.
- Async support — HTTPX documentation. Async clients and connection limits.
- Hypothesis — Hypothesis documentation. Property-based testing for arithmetic invariants.