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.

✓ Check yourself

Answer from memory before opening each one.

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

  6. 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