Quant Memo
Core

API Design for Research Libraries

Designing the functions a research team calls every day so the common case is one short line of obvious code, and the interface stays stable even as what happens underneath it changes.

Prerequisites: Anatomy of a Quant Research Environment

An internal research library gets called hundreds of times a day by people who mostly just want an answer, not a design lesson: give me this stock's daily returns, give me the rolling volatility, give me the sector for this ticker. If the function to do the first of those needs five arguments where three are almost always the same values, everyone will either copy-paste those three defaults everywhere or, worse, get one of them subtly wrong sometimes. Good API design for a research library means the common case reads as a single obvious line, while the underlying complexity — the data source, the caching, the edge cases — stays hidden unless someone specifically needs to reach past it.

The habits that make a library pleasant to use

Sensible defaults do most of the work: get_prices("AAPL") should return daily adjusted closes over some reasonable default window without the caller specifying an exchange calendar, an adjustment method, and a data vendor every single time — those become optional arguments for the rare case that needs something different. Consistent naming across functions matters more than it sounds: if one function takes a start_date and another takes a from_dt, callers waste time relearning the interface instead of just using it. And a stable interface — the function signature doesn't change even when the implementation underneath is rewritten — is what lets a library's internals improve over time (a faster backend, a new data source) without breaking every notebook that already calls it.

The opposite habits are what make a library actively unpleasant: a function that silently changes behavior depending on global state set somewhere else in the notebook, an error message that reports a stack trace instead of a plain sentence about what went wrong, or a return type that varies (sometimes a DataFrame, sometimes a dict) depending on inputs that look similar on the surface.

Worked example: two versions of the same function

A first version of a volatility function looks like compute_vol(df, col, window, annualize, trading_days, method, min_periods) — seven required arguments, and a new user has to read the source code just to call it correctly. A redesigned version looks like realized_vol(ticker, window=60) — two arguments, one with a sensible default, covering what 90% of callers actually want — with the rarer options (a different annualization convention, a non-default trading calendar) available as optional keyword arguments for the small fraction of callers who need them. The second version is strictly more capable, since everything the first could do is still reachable, but it's dramatically faster to use correctly for the common case, and far less likely to be called with an argument silently in the wrong order.

compute_vol(df, col, window, annualize, trading_days, method, min_periods) realized_vol(ticker, window=60)
Both functions can do the same work, but the second reaches the common case with two arguments instead of seven, pushing the rest into optional defaults.

What this means in practice

The payoff of good API design compounds because a research library is called far more often than it's written — time spent making the common call easy to get right pays back many times over across a team's daily use. It also changes what mistakes look like: a well-designed function tends to fail loudly and immediately when misused (a clear error on a bad ticker) rather than quietly returning a plausible-looking wrong answer, which is the difference between catching a bug in five minutes and finding it three months later in a published result.

A research library's API should make the common case a single obvious line with sensible defaults, keep naming consistent across functions, and keep its interface stable even as the implementation changes underneath — the goal is that callers rarely need to read the source code to use it correctly.

If a function needs more than two or three required arguments to cover the common case, that's usually a sign the defaults aren't doing enough work yet — push more of the decision into sensible defaults and leave the rest as optional.

Related concepts

Practice in interviews

Further reading

  • Ousterhout, A Philosophy of Software Design
ShareTwitterLinkedIn