Section C · Coding

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

The single most important rule in this chapter

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 Decimal from 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 contextvars to carry run_id across 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:

PatternWhen you reach for it
Hash tableReconcile two ledgers; group by composite key; dedupe by idempotency key.
Two-pointerSorted time-series alignment; merging two sorted feeds.
Prefix sumRunning balance over a transaction stream; period totals from a daily series.
Sliding windowRolling means, anomaly z-scores, treasury intraday monitoring.
Topological sortDependency-ordered execution of a close plan; resolving intercompany elimination order.
Bucketing / radixStratified sampling for SOX controls testing.
Counting / set opsSet 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:

  1. Restate the problem in your words.
  2. Clarify: input size, sorted?, currencies?, duplicates?, what if empty?
  3. Walk one example on a shared doc.
  4. Name the brute force and its complexity.
  5. Name the pattern ("two ledgers — I'd reach for a hash on the smaller side").
  6. Code with narration. Keep talking.
  7. Edge cases: empty, duplicate, single, very large, currency mismatch.
  8. 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.