Skip to main content
§ Notes

Comparing options-pricing libraries in Python.

QuantLib for the heavy machinery, vollib for fast vanilla pricing, and a custom numpy/scipy stack for when control matters more than features. Code-side comparison from the QuantSandbox toolchain.

by Bogdan#quant#python#options#comparison

If you're pricing options in Python, you have three reasonable shapes to choose between: a heavyweight library that covers everything (QuantLib), a lightweight library that covers vanilla pricing very well (vollib / py_vollib), or a custom stack on top of numpy / scipy / Cython for when control matters more than features.

This is the comparison from inside the QuantSandbox toolchain. We use all three at different points; the choice depends on what's being priced and why.

At a glance

LibraryWhat it coversPerformance shapeTrade-off
QuantLib (Python bindings)Vanilla, American, exotic, fixed income, term structure, calibration, multi-asset.C++ underneath; fast per call. SWIG-bound; vectorization is awkward.Large, somewhat C++-flavored API. Install non-trivial. Docs sparse.
vollib (py_vollib + py_lets_be_rational)Vanilla European: Black-Scholes, Black-76, generalized BS; Greeks; implied vol.Cython under the hood; very fast for vanilla and IV inversion.Vanilla only. No exotics, no calibration, no surface fitting.
Custom numpy/scipy/CythonWhatever you implement; common case is BS + Greeks, binomial trees, Monte Carlo.As fast as you make it; numpy vectorization is excellent for grids.Maintenance and testing burden. No community for edge cases.
Options-pricing libraries · what each is good at, what it costs.

The same call price, three ways

Closed-form Black-Scholes for a European call: spot 100, strike 100, 1y to expiry, 5% rate, 20% vol.

QuantLib:

import QuantLib as ql

today = ql.Date(26, 4, 2026)
ql.Settings.instance().evaluationDate = today
expiry = today + ql.Period(1, ql.Years)

spot = ql.SimpleQuote(100.0)
rate = ql.SimpleQuote(0.05)
vol  = ql.SimpleQuote(0.20)

spot_h  = ql.QuoteHandle(spot)
rate_ts = ql.YieldTermStructureHandle(
    ql.FlatForward(today, ql.QuoteHandle(rate), ql.Actual365Fixed())
)
vol_ts  = ql.BlackVolTermStructureHandle(
    ql.BlackConstantVol(today, ql.NullCalendar(), ql.QuoteHandle(vol), ql.Actual365Fixed())
)

process = ql.BlackScholesProcess(spot_h, rate_ts, vol_ts)
engine  = ql.AnalyticEuropeanEngine(process)

payoff   = ql.PlainVanillaPayoff(ql.Option.Call, 100.0)
exercise = ql.EuropeanExercise(expiry)
option   = ql.VanillaOption(payoff, exercise)
option.setPricingEngine(engine)

print(option.NPV())  # ~10.45

vollib:

from py_vollib.black_scholes import black_scholes

price = black_scholes(
    flag="c", S=100, K=100, t=1.0, r=0.05, sigma=0.20
)
print(price)  # ~10.45

Custom:

import numpy as np
from scipy.stats import norm

def bs_call(S, K, t, r, sigma):
    d1 = (np.log(S / K) + (r + 0.5 * sigma**2) * t) / (sigma * np.sqrt(t))
    d2 = d1 - sigma * np.sqrt(t)
    return S * norm.cdf(d1) - K * np.exp(-r * t) * norm.cdf(d2)

print(bs_call(100, 100, 1.0, 0.05, 0.20))  # ~10.45

The custom version is twelve lines shorter than the QuantLib version and runs faster than QuantLib for a single call (no Python-to-C++ marshalling). vollib's three-line version sits in the middle. Whether the brevity is worth what it gives up depends entirely on what you need next.

When to reach for QuantLib

The case for QuantLib is breadth. American options with discrete dividends. Bermudan options with a custom exercise schedule. Asian options. Barrier options with rebates. Forward-start, cliquet, look-back. Multi-asset. Bonds with embedded calls. Yield-curve construction with bootstrapping. Vol surface calibration to a market grid. SABR. Heston. Local volatility.

If the pricing problem is anything beyond a vanilla European, QuantLib is the path of least resistance. Implementing American options correctly is an afternoon's work to get the algorithm wrong and a week's work to get it right; QuantLib has had it right for fifteen years.

The cost is real. The Python bindings are SWIG-generated, which means the API has C++ texture all over it (QuoteHandle(SimpleQuote(...)), term structure handles, day count conventions explicit at every step). Documentation is mostly the C++ docs plus the example notebooks. The install is non-trivial — pip install QuantLib-Python works on most platforms now, but C++ build issues still happen. Date and calendar setup must be done early and consistently or pricing dates silently drift.

When the workload is building a vol surface, calibrating Heston, pricing a portfolio of exotics — reach for QuantLib and accept the API.

When to reach for vollib

The case for vollib is speed and simplicity on the vanilla path. py_vollib wraps py_lets_be_rational, which is Peter Jäckel's "Let's Be Rational" implied-volatility solver in Cython. For Black-Scholes pricing, Greeks, and (especially) implied-volatility inversion, it is among the fastest options on Python.

If the workload is price a million vanilla options, compute Greeks across a grid, invert IV from a market chain — vollib will do it in a tight loop faster than almost anything else, and the API is exactly as simple as it looks.

The constraints are also exactly what they look like. No American options. No exotics. No calibration. No surface fitting. The moment the workload steps outside vanilla European, vollib stops being applicable.

When to reach for a custom stack

The case for custom is control. Numpy is excellent at grids — pricing a 1000×1000 grid of (strike × maturity) points with vanilla Black-Scholes is one vectorized expression and runs in milliseconds. A custom binomial tree implementation can be tuned for a specific use case (specific dividend handling, specific exercise rules) in a way the library versions cannot. A Monte Carlo with antithetic variates, control variates, and a specific variance-reduction scheme is straightforward to write directly and very tedious to extract from a general-purpose library.

The cost: every line is yours to test and maintain. Every edge case (negative rates, near-zero vol, extremely-deep ITM, expiration handling) is yours to handle. The first three you'll catch; the fourth will bite you in production.

When the workload is a tight, specific computation that benefits from vectorization or specialization — write it. When it's anything where breadth or correctness across edge cases matters — don't.

A practical example: implied vol from a market chain

Inverting IV across a chain of 500 options is the workload that surfaces the differences sharply.

# vollib — fast, vanilla, two lines
from py_vollib.black_scholes.implied_volatility import implied_volatility

ivs = [
    implied_volatility(price, S, K, t, r, flag)
    for price, K, flag in chain
]

vollib does this in tens of milliseconds on a normal machine. QuantLib does it correctly but the per-call SWIG overhead and the term-structure setup add up. A custom Newton-Raphson loop is teachable but not as well-conditioned as let_be_rational for deep ITM / OTM where the BS price is numerically flat.

For implied vol on vanilla chains, vollib is the right answer. The QuantSandbox IV-surface module uses vollib for the inversion step, then hands the surface to QuantLib for parametric fitting (SVI, SABR) and to a custom layer for the visualization grid.

That's not a contrived example — it's the actual stack. Each library does the part it's good at. None of them does the whole thing.

The pick is per-problem

The honest answer to "which options-pricing library should I use" is: it depends on what you're pricing. Vanilla and IV inversion at scale: vollib. Anything beyond vanilla, or any work involving calibration / multi-asset / fixed income: QuantLib. Specific high-throughput grids or workload-specific optimization: custom.

Most production quant systems we've shipped end up using two of these together — vollib for the hot vanilla path, QuantLib for the breadth, occasionally custom code where one of them doesn't fit. Mixing is fine. Picking one and forcing every problem through it is the failure mode.