Architecture¶
High-level overview of PyQuantLib’s design: the problems it solves, the patterns it uses, and the reasoning behind key decisions.
See also
Internals for implementation details: BindingManager API, module patterns, trampoline code
Design Notes for deep-dive stories on specific binding challenges
Extending PyQuantLib for the Python subclassing guide
Building from Source for build setup
The Binding Challenge¶
Binding a C++ library to Python is usually mechanical: wrap a class, expose its methods, move on. QuantLib makes this harder than most libraries. Its C++ design relies on idioms that clash with Python’s memory model and runtime semantics.
The tensions that shaped PyQuantLib’s architecture:
Object lifetimes. QuantLib’s interpolation classes store iterators to external data, assuming the data outlives the interpolation. Python users pass temporary lists that die immediately. The binding layer must keep data alive without changing QuantLib’s API. See The Interpolation Binding Saga.
Singleton identity. QuantLib’s Settings singleton uses a C++ static local variable. When QuantLib is built as a shared library, dynamic linking can create duplicate singletons: Python writes the evaluation date to one, QuantLib reads from the other. The assignment succeeds, the getter returns the correct value, but all calculations use the wrong date. See The Settings Singleton Mystery.
Virtual dispatch across languages. Python subclasses can override QuantLib virtual methods via pybind11 trampolines. But accessing C++ objects from Python during these callbacks creates temporary wrappers that dangle after the expression ends. The trampoline design must enforce clean separation between C++ object management and Python computation. See The Python Subclassing Challenge.
Handle indirection. QuantLib uses Handle<T> extensively for lazy evaluation and relinking. This pattern is natural in C++ but adds friction in Python, where users expect to pass objects directly. See The Hidden Handle Pattern.
Import-time traps. QuantLib’s bridge-pattern classes (DayCounter, Calendar) have default constructors that create invalid objects. Using these as pybind11 default arguments crashes at import time, before any user code runs. See The Bridge Pattern Trap.
Protected member access. Python subclasses that override QuantLib virtual methods sometimes need access to C++ protected members. pybind11 lambdas cannot reach protected fields, and friend declarations require modifying QuantLib headers. A helper struct that inherits from the base class exploits C++ inheritance rules to expose the member without changing upstream code. See The Protected Member Problem.
Enum mutation. pybind11 represents enum values as singletons. Passing them by reference to C++ functions that modify the reference corrupts the singleton for the entire Python session. See The Enum Singleton Problem.
Cross-file type resolution. pybind11’s compile-time type resolution breaks when a method returns shared_ptr<T> for a type registered in a different source file. The solution requires deferring type conversion from compile time to runtime. See The Cross-Translation-Unit Holder Problem.
Diamond inheritance. QuantLib classes occasionally inherit from two base classes that share a common virtual ancestor, forming a diamond. pybind11’s classic holder system computes pointer offsets at compile time using static_cast, which cannot resolve the ambiguous paths through virtual bases. The solution requires migrating the entire inheritance chain to pybind11 3.0’s smart_holder system. See The Diamond Inheritance Problem.
Builder pattern translation. QuantLib’s Make* builders use C++ method chaining and implicit conversion operators to construct complex objects. Python has no implicit conversion operators, and method chaining with with* prefixes is not idiomatic when keyword arguments exist. PyQuantLib replaces the builder pattern with keyword-argument functions that return the result directly. See The Builder Pattern.
Reference member lifetimes. A few QuantLib classes store constructor arguments by reference rather than by value. In C++ the caller keeps the argument alive; in pybind11 the argument is a temporary that dies when the constructor returns, leaving a dangling reference inside the object. The binding must extend the argument’s lifetime without modifying QuantLib. See The Reference Member Trap.
These are not hypothetical risks. Each one was discovered through debugging production failures. The design notes document these investigations in full.
API Design Principle¶
A recurring question across these challenges is: how closely should PyQuantLib mirror QuantLib’s C++ API?
The answer: mirror the domain model, translate the idioms.
QuantLib’s class names (FixedRateBond, BlackScholesMertonProcess), method names (dayCounter(), nominal()), and class hierarchy are quantitative finance vocabulary. PyQuantLib preserves them exactly so that QuantLib documentation, textbooks, and community knowledge transfer directly.
But not everything in QuantLib’s API is a QuantLib concept. Handle<T> wrappers, builder method chaining, Null<Rate>() sentinels, and implicit conversion operators are C++ idioms – solutions to problems Python does not have. PyQuantLib replaces each with its Python equivalent: plain objects instead of handles, keyword arguments instead of method chaining, None instead of null sentinels.
The litmus test: Is this a QuantLib concept, or a C++ concept? If it is QuantLib, preserve it. If it is C++, find the Python equivalent. See API Design for the full story.
Layered Design¶
┌──────────────────────────────────────────────────────┐
│ Python application code │
│ import pyquantlib as ql │
├──────────────────────────────────────────────────────┤
│ pyquantlib Python package │
│ __init__.py · extensions/ · .pyi type stubs │
├──────────────────────────────────────────────────────┤
│ pybind11 binding layer (_pyquantlib) │
│ src/**/*.cpp · trampolines.h · binding_manager.h │
├──────────────────────────────────────────────────────┤
│ QuantLib C++ library (statically linked) │
│ ql/**/*.hpp · libQuantLib.a │
└──────────────────────────────────────────────────────┘
Each layer has a clear responsibility:
Layer |
Responsibility |
|---|---|
Python package |
Public API surface ( |
Binding layer |
Translates between C++ and Python. Manages object lifetimes, type conversions, handle wrapping, and trampoline dispatch. |
QuantLib |
All financial computation. Linked statically to ensure singleton correctness and eliminate runtime library dependencies. |
The binding layer exposes two Python namespaces:
pyquantlib(main namespace): Concrete classes ready for direct use:SimpleQuote,FlatForward,AnalyticEuropeanEngine, etc.pyquantlib.base: Abstract base classes for subclassing:Quote,CashFlow,PricingEngine, etc. Separated because ABCs are not meant to be instantiated directly; placing them in a submodule makes extending the library an explicit choice.
Module Organization¶
File Mapping¶
Each QuantLib header has a corresponding binding file. The directory structure mirrors QuantLib’s:
QuantLib Header |
PyQuantLib Binding |
|---|---|
|
|
|
|
|
|
|
|
Top-level QuantLib files (e.g., ql/instrument.hpp) map to src/core/. This 1:1 convention makes it straightforward to locate the binding for any QuantLib class.
Directory Structure¶
pyquantlib/
├── include/pyquantlib/
│ ├── binding_manager.h # Central orchestration
│ ├── trampolines.h # All trampoline classes
│ └── pyquantlib.h # Forward declarations
├── src/
│ ├── main.cpp # Module entry point
│ ├── submodules.cpp # Creates base submodule
│ ├── time/ # Time module bindings
│ │ ├── all.cpp # Aggregates time bindings
│ │ ├── date.cpp
│ │ ├── calendar.cpp
│ │ └── ...
│ ├── core/ # Core module bindings
│ ├── math/ # Math module bindings
│ └── ... # Other domain modules
├── pyquantlib/
│ ├── __init__.py # Python package init
│ ├── extensions/ # Pure Python extensions
│ └── _pyquantlib/ # Compiled extension
└── tests/ # Test files
Each domain directory follows the aggregator pattern: an all.cpp registers all binding functions for the directory, and individual .cpp files contain one class binding each. See Internals for the code patterns.
Test Organization¶
Test files group by QuantLib subdirectory:
QuantLib Path |
Test File |
|---|---|
|
|
|
|
|
|
Core Mechanisms¶
Two-Phase Initialization¶
pybind11 requires base classes to be registered before derived classes. In a project with hundreds of classes across many files, the ordering matters.
The BindingManager separates registration from execution. Each domain module calls addFunction to enqueue its binding functions, and a single finalize() call executes them all in insertion order. The ordering is manual but centralized: main.cpp lists modules in dependency order (patterns before quotes, quotes before instruments), and each module’s all.cpp lists its own classes in the right sequence. When the ordering is wrong, pybind11 raises an error at import time, making it straightforward to diagnose and fix.
The two-phase design also provides error isolation: if a binding fails, the error message identifies which registration caused it, rather than producing a cryptic pybind11 template error.
See Internals for the BindingManager API and macros.
Python Subclassing¶
QuantLib’s abstract base classes (Quote, CashFlow, PricingEngine, etc.) can be subclassed in Python. This enables rapid prototyping of custom pricing engines, term structures, and market data sources without C++ recompilation.
The mechanism is pybind11’s trampoline pattern: a C++ class intercepts virtual method calls and redirects them to Python:
QuantLib C++ code → Trampoline class → Python method
The key architectural constraint is that Python overrides should only receive simple types (numbers, enums, strings), not C++ objects. Accessing C++ objects from Python during callbacks creates temporary wrappers with dangerous lifetime semantics.
This is enforced by carefully choosing which virtual methods the trampoline exposes. For example, pricing engines expose the computational calculate(f1, f2, strike, ...) method (pure numbers) but not the parameter-extraction calculate() method (which accesses C++ handles and term structures).
All trampolines live in a single header (trampolines.h) to provide a clear inventory of overridable classes and methods.
See Extending PyQuantLib for the user guide and Internals for implementation details.
Implicit Conversion¶
Python types convert automatically to QuantLib types:
QuantLib Type |
Accepts |
|---|---|
|
|
|
Python lists, NumPy arrays |
|
Nested lists, 2D NumPy arrays |
Functions expecting Array accept plain Python lists, and dates can be passed as datetime.date objects. Conversions are registered via py::implicitly_convertible at the end of each binding file.
datetime.date objects also support arithmetic with Period via reverse operators:
import datetime
expiry = datetime.date(2025, 1, 15) + ql.Period("3M") # -> ql.Date(15, April, 2025)
start = datetime.date(2025, 6, 15) - ql.Period("1Y") # -> ql.Date(15, June, 2024)
See NumPy Interoperability for NumPy interoperability details.
Coverage¶
PyQuantLib binds a curated subset of QuantLib. Initial versions focused on the most widely used components. Coverage is actively growing. The API Reference provides the complete reference. Each new binding follows the checklist in Contributing.
Design Decisions¶
Why pybind11?¶
The existing Python bindings for QuantLib use SWIG (QuantLib-SWIG) and Cython (PyQL). Both introduce an additional language beyond C++ and Python: SWIG’s interface definition files or Cython’s hybrid syntax. This increases cognitive load when navigating binding code or keeping wrappers synchronized with upstream QuantLib changes.
pybind11 bindings are written in standard C++. The wrapper code directly references QuantLib headers and classes, with no intermediate DSL, code generation step, or third language to learn. A developer who knows C++ and Python can read and modify bindings immediately.
Additional factors:
Compile-time type safety: Signature mismatches fail at build time, not runtime
Standard debugging: C++ debuggers work directly on the binding code, with no generated-code layers obscuring the call stack
Active ecosystem: Extensive documentation and community support
Why Static Linking?¶
QuantLib’s Settings singleton uses a C++ static local variable. When QuantLib is built as a shared library and loaded into a Python extension module, dynamic linking can create duplicate singleton instances. The Python binding writes the evaluation date to one instance; QuantLib’s pricing code reads from another. The assignment succeeds, the getter returns the correct value, but all calculations use the wrong date.
Static linking embeds QuantLib directly into the Python extension module, guaranteeing a single singleton instance. This also simplifies deployment by eliminating dynamic library dependencies.
See The Settings Singleton Mystery for the full investigation.
Why a Base Submodule?¶
Abstract base classes live in pyquantlib.base, separate from the main namespace. This signals that these classes are for subclassing, not direct instantiation. A user writing from pyquantlib.base import Quote is making an explicit choice to extend the library, while import pyquantlib as ql provides only concrete, ready-to-use classes.
Why Two-Phase Initialization?¶
pybind11 requires base classes to be bound before derived classes. Without centralized ordering, adding a new class means reasoning about the ordering of every PYBIND11_MODULE call and hoping no file gets compiled in the wrong sequence. The BindingManager collects all registrations first, then executes them in insertion order via finalize(). The ordering is still manual – the developer arranges modules in main.cpp and classes within each all.cpp – but it is centralized in two visible places rather than scattered across the build system. Clear error messages identify which binding failed.
Why a Single Trampolines Header?¶
All trampoline classes are in include/pyquantlib/trampolines.h. This provides a single source of truth for which classes are overridable from Python and which virtual methods are exposed. When QuantLib updates a virtual method signature, there is one place to update. The file also serves as documentation: a complete inventory of Python-extensible classes.
Why 1:1 File Mapping?¶
Each QuantLib header maps to exactly one binding file. This convention makes it trivial to find the binding for any QuantLib class (replace ql/ with src/ and .hpp with .cpp). It also prevents binding files from growing into unmanageable monoliths and keeps compilation units small for faster incremental builds.
The trade-off is that methods returning types from other files encounter pybind11’s cross-translation-unit type resolution limitation. This is handled with a py::cast() pattern documented in The Cross-Translation-Unit Holder Problem.