APIs, Regex & Functional Python

REST clients, data validation, higher-order functions, and concurrency

Every production Python system in finance touches external data: market prices from Bloomberg or Refinitiv, order confirmations from broker APIs, reference data from internal microservices. This chapter teaches you to consume REST APIs professionally — authentication, error recovery, rate limiting, and session management — the same patterns used in trading desks and fintech platforms worldwide.

Regular expressions are the standard tool for validating and extracting structured data from unstructured text. You will write patterns to validate ISINs, BIC codes, trade references, and monetary amounts — and build a reusable validator that can be dropped into any data ingestion pipeline.

Then functional programming: map, filter, and reduce compose clean, testable data-transformation pipelines without mutation or loops. Finally, Python's concurrency model — the GIL, when threading beats multiprocessing, and the concurrent.futures interface that unifies both — equips you to write systems that fetch data from dozens of APIs simultaneously or parallelise risk calculations across CPU cores.

Consuming REST APIs — Market Data & Order Management

The requests library is the industry standard for HTTP in Python. Every major financial data provider — Bloomberg, Refinitiv, Alpha Vantage, Alpaca, Interactive Brokers — exposes a REST API, and requests is how you talk to them.

  • GET — read data: price quotes, positions, account balances
  • POST — write data: place orders, submit confirmations
  • Headers — pass authentication: Authorization: Bearer <token>
  • Timeout — always set one; a hanging request blocks the thread indefinitely
  • response.raise_for_status() — raises HTTPError on 4xx/5xx; never ignore status codes

Why it matters: In live trading, a missed timeout or unchecked 401 can leave an order unacknowledged. Production systems treat every API call as potentially failing and code accordingly.

Python
import requests API_BASE = "https://api.marketdata.example.com/v1"API_KEY  = "your-api-key-here" HEADERS = {    "Authorization": f"Bearer {API_KEY}",    "Accept":        "application/json",    "X-Client-Id":  "trading-desk-py",} def get_quotes(symbols: list[str]) -> list[dict]:    """    Fetch real-time quotes for a list of ticker symbols.    Returns a list of quote dicts; raises on HTTP errors.    """    url = f"{API_BASE}/quotes"    params = {"symbols": ",".join(symbols)}     response = requests.get(        url,        headers=HEADERS,        params=params,        timeout=(3.05, 10),   # (connect timeout, read timeout) — always set both    )    response.raise_for_status()   # raises HTTPError on 4xx / 5xx     data = response.json()        # parse JSON body    return data["quotes"]         # extract the list we care about  def place_order(symbol: str, side: str, qty: int, order_type: str = "MARKET") -> dict:    """    Submit an equity order via POST. Returns the broker's order acknowledgement.    """    payload = {        "symbol":     symbol,        "side":       side,       # "BUY" | "SELL"        "quantity":   qty,        "order_type": order_type,    }    response = requests.post(        f"{API_BASE}/orders",        headers=HEADERS,        json=payload,             # sets Content-Type: application/json automatically        timeout=(3.05, 10),    )    response.raise_for_status()    return response.json()  # ── Usage ─────────────────────────────────────────────────────────────────────# quotes = get_quotes(["AAPL", "MSFT", "NVDA", "TSLA"])# for q in quotes:#     print(f"{q['symbol']:<6}  bid={q['bid']:>8.2f}  ask={q['ask']:>8.2f}") # ack = place_order("AAPL", "BUY", 100)# print(f"Order {ack['order_id']} accepted — status: {ack['status']}")

Robust API Clients — Retries, Rate Limits & Sessions

Production API clients must handle three failure modes gracefully: transient network errors, server overload (429), and authentication expiry. A bare requests.get() call handles none of them.

  • requests.Session — reuses the underlying TCP connection; significant latency saving for high-frequency polling
  • urllib3.Retry + HTTPAdapter — automatic retries with exponential backoff; mount on the session
  • 429 Too Many Requests — the server's rate-limit signal; respect the Retry-After header
  • Never retry on 400 (bad request) or 401 (auth failure) — those are your bugs, not transient
  • Context manager — wrap the session in with so the connection pool is always released

Best practice: wrap every external API call in a client class. It centralises auth, retry logic, and base-URL configuration — making it trivial to swap providers or mock in tests.

Python
import timeimport requestsfrom requests.adapters import HTTPAdapterfrom urllib3.util.retry import Retry class MarketDataClient:    """    Production-grade API client with connection reuse, automatic retries,    and rate-limit handling.  One instance per application — not per request.    """     _RETRY_STRATEGY = Retry(        total=3,        backoff_factor=0.5,         # waits 0.5s, 1s, 2s between retries        status_forcelist=[500, 502, 503, 504],   # retry on server errors only        allowed_methods=["GET"],    # never auto-retry POST (not idempotent)        raise_on_status=False,    )     def __init__(self, base_url: str, api_key: str):        self.base_url = base_url.rstrip("/")        self._session = requests.Session()        self._session.headers.update({            "Authorization": f"Bearer {api_key}",            "Accept":        "application/json",        })        adapter = HTTPAdapter(max_retries=self._RETRY_STRATEGY)        self._session.mount("https://", adapter)        self._session.mount("http://",  adapter)     def __enter__(self):        return self     def __exit__(self, *_):        self._session.close()     def get(self, path: str, **kwargs) -> dict:        url = f"{self.base_url}/{path.lstrip('/')}"        resp = self._session.get(url, timeout=(3.05, 10), **kwargs)         if resp.status_code == 429:            wait = int(resp.headers.get("Retry-After", 60))            print(f"[rate-limit] sleeping {wait}s")            time.sleep(wait)            resp = self._session.get(url, timeout=(3.05, 10), **kwargs)         resp.raise_for_status()        return resp.json()  # ── Usage ─────────────────────────────────────────────────────────────────────# with MarketDataClient("https://api.example.com/v1", api_key="...") as client:#     quotes  = client.get("/quotes", params={"symbols": "AAPL,MSFT"})#     history = client.get("/history/AAPL", params={"days": 30})

Regular Expressions — Core Patterns

The re module gives you a mini-language for describing text patterns. In financial systems, regex is the standard tool for parsing Bloomberg terminal output, validating reference data, and extracting fields from unstructured trade confirmations.

  • re.match(pattern, s) — matches only at the start of the string; returns a Match object or None
  • re.search(pattern, s) — finds the first match anywhere in the string
  • re.findall(pattern, s) — returns a list of all non-overlapping matches
  • re.sub(pattern, repl, s) — replace every match with repl
  • re.compile(pattern) — pre-compiles for reuse; essential in loops over millions of records

Key syntax: . any char · \d digit · \w word char · [A-Z] character class · {n,m} quantifier · ^/$ anchors · () capture group

Best practice: always re.compile() patterns that are used more than once. The compiled regex is cached by the interpreter, but explicit compilation makes intent clear and avoids accidental recompilation inside hot loops.

Python
import re # ── 1. re.compile — pre-compile for reuse ────────────────────────────────────CURRENCY_CODE = re.compile(r"[A-Z]{3}")   # ISO 4217 currency code line = "FX spot: USD/GBP 0.7852  EUR/USD 1.0834  JPY/USD 0.0067"currencies = CURRENCY_CODE.findall(line)print("Currencies found:", currencies)    # ['USD', 'GBP', 'EUR', 'USD', 'JPY', 'USD'] # ── 2. re.match vs re.search ──────────────────────────────────────────────────TICKER = re.compile(r"[A-Z]{1,5}") print(re.match(r"[A-Z]{1,5}", "AAPL.L"))     # Match — starts with uppercaseprint(re.match(r"[A-Z]{1,5}", "Price: AAPL")) # None  — doesn't start with itprint(re.search(r"[A-Z]{1,5}", "Price: AAPL"))# Match — found at offset 7 # ── 3. re.findall with groups — extract bid/ask pairs ────────────────────────QUOTE_PATTERN = re.compile(r"([A-Z]{3,4}):s*([d.]+)/([d.]+)")feed = "EURUSD: 1.0832/1.0835  GBPUSD: 1.2701/1.2704  USDJPY: 149.82/149.85" for m in QUOTE_PATTERN.finditer(feed):    pair, bid, ask = m.group(1), m.group(2), m.group(3)    print(f"{pair}  bid={bid}  ask={ask}") # ── 4. re.sub — normalise noisy Bloomberg ticker format ──────────────────────SUFFIX = re.compile(r"s+(Equity|Corp|Govt|Index)$", re.IGNORECASE)raw_tickers = ["AAPL US Equity", "BP/ LN Equity", "T 4.5 01/15/2030 Corp"]clean = [SUFFIX.sub("", t).strip() for t in raw_tickers]print("Cleaned tickers:", clean)    # ['AAPL US', 'BP/ LN', 'T 4.5 01/15/2030']

Regex for Financial Data Validation

Financial systems live and die on reference data quality. A single malformed ISIN, invalid BIC, or bad trade ID can fail a settlement instruction and trigger a financial penalty. Regex validation at the point of ingestion — before the data reaches the database — is standard practice.

  • ISIN — 12 chars: 2-letter country code + 9 alphanumeric + 1 check digit: ^[A-Z]{2}[A-Z0-9]{9}[0-9]$
  • BIC/SWIFT — 8 or 11 chars: 4-letter bank + 2-letter country + 2-char location + optional 3-char branch: ^[A-Z]{4}[A-Z]{2}[A-Z0-9]{2}([A-Z0-9]{3})?$
  • Trade reference — firm-specific; typically prefix + date + sequence: ^TRD-\d{8}-\d{6}$
  • Monetary amount — optional currency + digits + optional decimal: ^[A-Z]{0,3}\s*\d{1,15}(\.\d{1,4})?$

Production pattern: build a DataValidator class with a registry of compiled patterns. Add a validate(field, value) method that returns (is_valid, reason). This makes validation rules explicit, testable, and swappable without touching business logic.

Python
import re class DataValidator:    """    Registry of compiled regex patterns for financial reference data.    Returns (True, None) on success or (False, reason) on failure.    Use in data-ingestion pipelines before writing to the database.    """     _PATTERNS = {        "isin":        re.compile(r"^[A-Z]{2}[A-Z0-9]{9}[0-9]$"),        "bic":         re.compile(r"^[A-Z]{4}[A-Z]{2}[A-Z0-9]{2}([A-Z0-9]{3})?$"),        "trade_ref":   re.compile(r"^TRD-d{8}-d{6}$"),        "ticker":      re.compile(r"^[A-Z]{1,5}$"),        "currency":    re.compile(r"^[A-Z]{3}$"),        "amount":      re.compile(r"^d{1,15}(.d{1,4})?$"),    }     @classmethod    def validate(cls, field: str, value: str) -> tuple[bool, str | None]:        pattern = cls._PATTERNS.get(field)        if pattern is None:            return False, f"Unknown field type: {field}"        if pattern.match(value):            return True, None        return False, f"'{value}' is not a valid {field}"     @classmethod    def validate_trade(cls, trade: dict) -> list[str]:        """Validate a full trade dict; return list of error strings."""        checks = [            ("isin",      trade.get("isin", "")),            ("bic",       trade.get("counterparty_bic", "")),            ("trade_ref", trade.get("ref", "")),            ("currency",  trade.get("ccy", "")),            ("amount",    str(trade.get("notional", ""))),        ]        errors = []        for field, value in checks:            ok, msg = cls.validate(field, value)            if not ok:                errors.append(msg)        return errors  # ── Usage ─────────────────────────────────────────────────────────────────────trades = [    {"ref": "TRD-20240315-000001", "isin": "GB00B15KXQ89", "ccy": "GBP",     "notional": "250000.00", "counterparty_bic": "BARCGB22"},    {"ref": "TRD-BAD-REF",         "isin": "US0378331005", "ccy": "USD",     "notional": "175000.00", "counterparty_bic": "CHAS"},       # bad ref + bad BIC] for t in trades:    errors = DataValidator.validate_trade(t)    status = "PASS" if not errors else "FAIL"    print(f"{t['ref']:<28}  {status}")    for e in errors:        print(f"  -> {e}")

Functional Programming — map, filter, reduce

Functional programming centres on pure functions — functions that produce the same output for the same input and have no side effects. Applied to collections, three higher-order functions cover most transformation needs.

  • map(fn, iterable) — apply fn to every element; returns a lazy iterator. Use when you want to transform every record in a stream without materialising the whole list in memory
  • filter(pred, iterable) — keep only elements where pred(element) is True; also lazy. Use to screen out invalid records before expensive processing
  • functools.reduce(fn, iterable) — fold the sequence into a single value left-to-right. Use for aggregations: total P&L, sum of notionals, running max

map/filter vs list comprehensions:

  • Comprehension [f(x) for x in xs if pred(x)] — preferred for readability in most cases
  • map/filter — preferred for lazy pipelines on large datasets, or when chaining with other higher-order functions

Best practice: name your transformation functions rather than writing complex lambdas inline. map(mark_to_market, positions) reads like documentation; map(lambda p: {**p, "pnl": (p["price"] - p["avg"]) * p["qty"]}, positions) does not.

Python
from functools import reduce # ── Portfolio positions ────────────────────────────────────────────────────────positions = [    {"sym": "AAPL", "qty": 100, "avg_price": 172.50, "current_price": 186.55},    {"sym": "MSFT", "qty":  50, "avg_price": 380.00, "current_price": 415.30},    {"sym": "TSLA", "qty":  40, "avg_price": 225.00, "current_price": 195.40},    {"sym": "NVDA", "qty":  30, "avg_price": 480.00, "current_price": 520.00},    {"sym": "META", "qty":  25, "avg_price": 510.00, "current_price": 487.20},] # ── 1. map — mark every position to market ────────────────────────────────────def mark_to_market(pos: dict) -> dict:    pnl = round((pos["current_price"] - pos["avg_price"]) * pos["qty"], 2)    pnl_pct = round((pos["current_price"] / pos["avg_price"] - 1) * 100, 2)    return {**pos, "pnl": pnl, "pnl_pct": pnl_pct} marked = list(map(mark_to_market, positions)) # ── 2. filter — isolate losing positions > 2% down ───────────────────────────losers = list(filter(lambda p: p["pnl_pct"] < -2.0, marked)) # ── 3. reduce — aggregate total portfolio P&L ────────────────────────────────total_pnl = reduce(lambda acc, p: acc + p["pnl"], marked, 0.0) # ── 4. Print results ──────────────────────────────────────────────────────────print(f"{'Symbol':<6} {'Qty':>5} {'Avg':>8} {'Price':>8} {'P&L':>10} {'%':>7}")print("-" * 50)for p in marked:    sign = "+" if p["pnl"] >= 0 else ""    print(f"{p['sym']:<6} {p['qty']:>5} {p['avg_price']:>8.2f} "          f"{p['current_price']:>8.2f} {sign}{p['pnl']:>9,.2f} {sign}{p['pnl_pct']:>6.2f}%")print("-" * 50)sign = "+" if total_pnl >= 0 else ""print(f"{'TOTAL P&L':>42}  {sign}{total_pnl:>9,.2f}") print()print("Positions down > 2%:")for p in losers:    print(f"  {p['sym']}: {p['pnl_pct']:.2f}%  ({p['pnl']:,.2f})")

Concurrency — GIL, Threading & Multiprocessing

The Global Interpreter Lock (GIL) is a mutex inside CPython that ensures only one thread executes Python bytecode at a time. It protects CPython's reference-counting memory manager from corruption. The GIL releases in two situations: (1) after every ~5ms (configurable via sys.setswitchinterval()), giving other threads a chance to run; (2) whenever a thread makes a blocking system call — network I/O, file I/O, time.sleep. This means threading is effective for I/O-bound work but provides zero CPU parallelism. Python 3.13 introduced an experimental --disable-gil mode, but it is not yet production-ready.

Three concurrency models — choose by task type:

  • threading — multiple threads in one process, shared memory, GIL applies. Best for I/O-bound tasks (API calls, DB queries, file I/O). Low overhead (~8KB per thread). Use threading.Thread for direct control or ThreadPoolExecutor for managed pools.
  • multiprocessing — multiple OS processes, each with its own GIL and memory space. Best for CPU-bound tasks (Monte Carlo, backtesting, numerical computation). Higher overhead (~50MB per process). Use ProcessPoolExecutor for managed pools.
  • asyncio — single thread, cooperative (coroutine-based) concurrency. No GIL concerns. Best for very high-volume I/O (thousands of simultaneous connections, WebSocket streams). Requires the full call stack to use async-aware libraries (aiohttp, asyncpg).

threading.Thread — direct thread control:

  • t = threading.Thread(target=fn, args=(a, b)) — creates a thread
  • t.start() — launches the thread; returns immediately
  • t.join() — blocks the caller until thread t finishes
  • t.daemon = True — mark as daemon: thread dies when main program exits (useful for background monitors)
  • Best practice: always .join() non-daemon threads before program exit to avoid lost work

Thread safety — race conditions and synchronisation: When multiple threads read-modify-write shared state simultaneously, the result is non-deterministic. Python's += is not atomic — it compiles to multiple bytecodes and another thread can interrupt mid-operation.

  • threading.Lock() — mutual exclusion. Use as a context manager: with lock: .... Only one thread can hold the lock at a time; others block until it is released.
  • threading.RLock() — reentrant lock; the same thread can acquire it multiple times without deadlocking.
  • threading.Queue — thread-safe FIFO queue. The standard tool for producer-consumer pipelines between threads. queue.put(item) / queue.get() are both thread-safe. Prefer Queue over Lock+list for passing data between threads.
  • Best practice: minimise shared mutable state. Prefer passing results back through a Queue or by returning from future.result() rather than writing to shared variables with locks.

concurrent.futures — the modern unified interface:

  • ThreadPoolExecutor(max_workers=N) — reusable thread pool; threads are created once and reused across tasks
  • ProcessPoolExecutor(max_workers=N) — reusable process pool. Use os.cpu_count() as the default for CPU-bound work
  • executor.submit(fn, *args) — schedules fn(*args) and returns a Future immediately
  • executor.map(fn, iterable) — like built-in map but parallel; yields results in order
  • as_completed(futures) — yields Future objects as they complete (fastest-first, not submission order)
  • future.result() — blocks until done and returns the value, or re-raises any exception from the worker
  • Use as context manager (with executor:) to ensure clean shutdown and thread/process cleanup

asyncio — cooperative concurrency:

  • async def fn(): — defines a coroutine. Calling it returns a coroutine object (not a result); must be awaited or gathered
  • await expr — suspends the current coroutine and yields control to the event loop until expr completes
  • asyncio.gather(*coroutines) — runs multiple coroutines concurrently in the event loop; returns all results when all complete
  • asyncio.run(main()) — creates an event loop, runs the coroutine, closes the loop (use this as the entry point)
  • Key constraint: never call blocking functions (time.sleep, requests.get) inside an async function — they block the event loop and defeat the purpose. Use await asyncio.sleep() and async libraries like aiohttp instead

Production decision table:

  • Parallel API calls (10–100 endpoints) → ThreadPoolExecutor
  • Parallel DB queries across microservices → ThreadPoolExecutor
  • Monte Carlo VaR, backtesting, scenario analysis → ProcessPoolExecutor
  • NumPy/Pandas matrix operations → NumPy/C extensions (release GIL internally)
  • 10,000+ simultaneous connections, WebSocket streams → asyncio + aiohttp
  • Producer-consumer pipeline between threads → threading.Queue
  • Background monitoring / heartbeat → daemon Thread
Python
import time, threading, randomfrom concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor, as_completed EXCHANGES = ["NYSE", "LSE", "XETRA", "TSE", "ASX"]PRICES    = {"NYSE":186.55,"LSE":147.30,"XETRA":152.40,"TSE":143.90,"ASX":135.00} # ─────────────────────────────────────────────────────────────────# PART 1 — threading.Thread: direct lifecycle control# ─────────────────────────────────────────────────────────────────quotes = [] def fetch_quote(exchange):    time.sleep(0.1)             # simulate 100ms network latency    quotes.append({"exchange": exchange, "price": PRICES[exchange]}) threads = [threading.Thread(target=fetch_quote, args=(ex,)) for ex in EXCHANGES]t0 = time.time()for t in threads: t.start()    # launch all 5 threadsfor t in threads: t.join()     # wait for all to finishelapsed = round((time.time() - t0) * 1000)print(f"=== threading.Thread: 5 exchanges in {elapsed}ms (vs ~500ms sequential) ===")for r in sorted(quotes, key=lambda x: x["exchange"]):    print(f"  {r['exchange']:<8}  {r['price']:>8.2f}")  # ─────────────────────────────────────────────────────────────────# PART 2 — Thread safety: Lock prevents race conditions# ─────────────────────────────────────────────────────────────────total_notional = 0.0lock = threading.Lock() def book_trade(qty, price):    """Without lock, concurrent += on total_notional is non-deterministic."""    global total_notional    notional = qty * price    time.sleep(0.005)    with lock:                  # only one thread enters this block at a time        total_notional += notional trades = [(100,186.55),(50,415.30),(30,520.00),(20,195.40),(25,487.20)]workers = [threading.Thread(target=book_trade, args=t) for t in trades]for w in workers: w.start()for w in workers: w.join()expected = sum(q * p for q, p in trades)print(f"\n=== Lock: total notional = {total_notional:,.2f}  (expected {expected:,.2f}) ===")  # ─────────────────────────────────────────────────────────────────# PART 3 — threading.Queue: thread-safe producer-consumer pipeline# ─────────────────────────────────────────────────────────────────import queue as queue_mod trade_queue = queue_mod.Queue()validated   = []errors_q    = [] def producer():    """Generates trade records and puts them onto the queue."""    raw_trades = [        {"id":"T001","sym":"AAPL","qty":100,"price":186.55},        {"id":"T002","sym":"MSFT","qty": 50,"price":415.30},        {"id":"T003","sym":"????","qty": -5,"price":520.00},  # invalid        {"id":"T004","sym":"NVDA","qty": 30,"price":520.00},    ]    for t in raw_trades:        trade_queue.put(t)    trade_queue.put(None)       # sentinel — signals consumer to stop def consumer():    """Validates each trade taken from the queue."""    while True:        item = trade_queue.get()        if item is None:            break        if item["qty"] > 0 and item["sym"].isalpha():            validated.append(item)        else:            errors_q.append(item["id"])        trade_queue.task_done() p = threading.Thread(target=producer)c = threading.Thread(target=consumer)p.start(); c.start()p.join();  c.join()print(f"\n=== Queue pipeline: {len(validated)} valid, {len(errors_q)} rejected ({errors_q}) ===")  # ─────────────────────────────────────────────────────────────────# PART 4 — ThreadPoolExecutor: recommended pool for I/O tasks# ─────────────────────────────────────────────────────────────────SERVICES = {    "price-svc":    (200, {"AAPL":186.55,"MSFT":415.30}),    "risk-svc":     (200, {"AAPL":0.25,  "MSFT":0.18 }),    "fx-svc":       (200, {"GBPUSD":1.27,"EURUSD":1.09}),    "news-svc":     (429, None),   # rate-limited    "ref-data-svc": (200, {"AAPL":"US0378331005"}),} def call_service(name):    time.sleep(0.08)    status, data = SERVICES[name]    if status != 200:        raise RuntimeError(f"HTTP {status}")    return name, data t0 = time.time()svc_results = {}print("\n=== ThreadPoolExecutor: 5 microservices ===")with ThreadPoolExecutor(max_workers=5) as pool:    future_map = {pool.submit(call_service, svc): svc for svc in SERVICES}    for future in as_completed(future_map):        svc = future_map[future]        try:            name, data = future.result()    # re-raises worker exception if any            svc_results[name] = data            print(f"  OK  {name}")        except Exception as e:            print(f"  ERR {svc}: {e}")elapsed = round((time.time() - t0) * 1000)print(f"All 5 services queried in {elapsed}ms")  # ─────────────────────────────────────────────────────────────────# PART 5 — ProcessPoolExecutor: CPU-bound Monte Carlo VaR# Bypasses GIL: each worker is a separate OS process# ─────────────────────────────────────────────────────────────────def mc_path(seed):    """One simulation path — pure CPU, no I/O, no GIL release."""    random.seed(seed)    v = 1_000_000.0    for _ in range(5_000):        v *= 1 + random.gauss(0.0005, 0.015)    return v # ThreadPoolExecutor would NOT speed this up — GIL blocks parallel Python bytecodewith ProcessPoolExecutor(max_workers=4) as pool:    paths = sorted(pool.map(mc_path, range(200))) var_95 = 1_000_000 - paths[int(0.05 * len(paths))]print(f"\n=== ProcessPoolExecutor: Monte Carlo VaR (200 paths, 4 cores) ===")print(f"  95% 1-day VaR : ${var_95:,.0f}")print(f"  Min portfolio : ${paths[0]:,.0f}   Max: ${paths[-1]:,.0f}")  # ─────────────────────────────────────────────────────────────────# PART 6 — asyncio: cooperative concurrency (single-threaded, GIL-irrelevant)# ─────────────────────────────────────────────────────────────────import asyncio async def async_fetch(symbol, latency_ms=80):    """Simulates async HTTP call — yields control during I/O wait."""    await asyncio.sleep(latency_ms / 1000)   # non-blocking wait    prices = {"AAPL":186.55,"MSFT":415.30,"NVDA":520.00,"AMZN":175.50}    return symbol, prices[symbol] async def fetch_all():    t0 = time.time()    # asyncio.gather runs all coroutines concurrently in the event loop    results = await asyncio.gather(        async_fetch("AAPL"),        async_fetch("MSFT"),        async_fetch("NVDA"),        async_fetch("AMZN"),    )    elapsed = round((time.time() - t0) * 1000)    print(f"\n=== asyncio.gather: 4 async fetches in {elapsed}ms ===")    for sym, price in results:        print(f"  {sym:<5}  {price:>8.2f}") asyncio.run(fetch_all())
PythonRuns entirely in your browser — nothing is sent to a server.

Practice Questions

Question 1

What does re.match("GBP", "Rate: GBP 1.25") return?

  • A Match object — GBP appears in the string
  • None — re.match only checks from the start of the string
  • A Match object at position 6
  • Raises re.error because the pattern has no anchors

Question 2

What does map(float, ["183.50", "415.20", "520.00"]) return in Python 3?

  • A list: [183.5, 415.2, 520.0]
  • A lazy map object — you must wrap it in list() to materialise it
  • A generator expression
  • A tuple of floats

Question 3

A risk system runs Monte Carlo simulations using 4 CPU cores. Which concurrency tool will actually achieve parallel execution despite the GIL?

  • threading.Thread — threads share memory and run in parallel
  • concurrent.futures.ThreadPoolExecutor — thread pool is fastest
  • concurrent.futures.ProcessPoolExecutor — each worker is a separate process with its own GIL
  • asyncio.gather — async functions bypass the GIL for CPU work

Question 4

How many characters does a valid ISIN contain, and what is the structure?

  • 9 characters: 2-letter country + 7 alphanumeric
  • 12 characters: 2-letter country code + 9 alphanumeric + 1 check digit
  • 11 characters: 4-letter exchange + 7-digit local code
  • 12 characters: 3-letter currency + 9-digit security ID

Question 5

What does reduce(lambda a, b: a + b, [10_000, 25_000, 15_000]) return?

  • 10000
  • [10000, 25000, 15000]
  • 50000
  • Raises TypeError — reduce needs an initial value

Question 6

A market data API returns HTTP 429. What is the correct production response?

  • Immediately retry with the same request
  • Log the error, raise an exception, and stop polling
  • Respect the Retry-After header delay, then retry with exponential backoff
  • Switch to a different API endpoint and abandon the current one

Question 7

What does list(filter(None, [0, 1, "", "AAPL", False, 42, None])) return?

  • [1, "AAPL", 42] — removes all falsy values
  • [0, "", False, None] — keeps only falsy values
  • [None] — only removes None
  • Raises TypeError — filter requires a boolean function

Question 8

Why is threading effective for parallelising API calls to 10 exchanges simultaneously, despite the GIL?

  • The GIL is automatically disabled when using threading.Thread
  • Threads release the GIL while waiting on network or disk I/O, so other threads can run
  • Python 3.12+ removed the GIL entirely
  • API calls run in C extensions which always bypass the GIL