Internals

Implementation reference for PyQuantLib’s binding infrastructure: the BindingManager API, module patterns, trampoline implementation, and handle templates.

See also

Architecture for high-level design and rationale, Extending PyQuantLib for the Python subclassing guide, Contributing for development workflow.

BindingManager

The BindingManager class is the central orchestrator for module organization and binding registration.

Purpose

  1. Two-phase initialization: Collects binding functions first, executes them all via finalize()

  2. Submodule management: Creates and tracks submodules (pyquantlib.base)

  3. Error isolation: Wraps each binding execution in try/catch with descriptive error messages

  4. sys.modules registration: Ensures proper Python import behavior

  5. Helper utilities: Provides bindHandle<T> and bindRelinkableHandle<T> templates

Key Methods

class BindingManager {
public:
    // Register a binding function for later execution
    void addFunction(void (*register_func)(py::module_&),
                     py::module_& target_module,
                     const std::string& description = "");

    // Create or retrieve a submodule
    py::module_ getOrCreateSubmodule(const std::string& name,
                                     const std::string& doc = "");

    // Execute all registered bindings
    void finalize();
};

Usage Pattern

// In main.cpp
BindingManager manager(m, "pyquantlib");

// Ordering is manual: modules listed in dependency order.
// patterns before quotes (Observable before Quote),
// quotes before instruments (Quote before Instrument), etc.
submodules_bindings(manager);   // Creates "base" submodule first
patterns_bindings(manager);     // Observer/Observable
time_bindings(manager);         // Date, Calendar, etc.
// ... other modules

manager.finalize();             // Execute all bindings in insertion order

Ordering

finalize() executes binding functions in insertion order. There is no automatic dependency resolution – the developer is responsible for arranging modules in main.cpp and classes within each all.cpp so that base classes are registered before derived classes.

In practice, module boundaries do most of the work. patterns_bindings (Observable) naturally runs before quotes_bindings (Quote) because the listing in main.cpp follows the inheritance hierarchy. Within each all.cpp, the entries are short enough to order by inspection.

When the ordering is wrong, pybind11 raises an error at import time. The BindingManager’s error isolation identifies the failing binding by its description string, making it straightforward to diagnose and fix by reordering.

What Must Be Ordered vs. What Resolves Lazily

Not all type references require strict ordering. The rule is:

Parent class registration must precede child class registration. When pybind11 encounters py::class_<Child, Parent>, it looks up the Parent type immediately. If Parent is not yet registered, pybind11 raises an error. This is the only hard ordering constraint.

Parameter types, return types, and holder types resolve lazily at Python call time. When .def(py::init<Args...>()) or .def("method", &Class::method) is called, pybind11 stores a function object but does not check that the argument or return types are registered. The actual type_caster lookup happens when Python code invokes the function. Since finalize() completes all registrations before any Python code runs, cross-module type references in function signatures work without ordering constraints.

This is why IborIndex (registered in indexes_bindings) can accept Handle<YieldTermStructure> and shared_ptr<YieldTermStructure> parameters even though YieldTermStructure is registered later in termstructures_bindings. The same applies to inflation indexes referencing inflation term structure types.

In summary:

Reference kind

Resolution timing

Ordering required?

py::class_<Child, Parent>

Immediate (registration time)

Yes – parent first

Constructor/method parameter types

Lazy (Python call time)

No

Method return types

Lazy (Python call time)

No

py::implicitly_convertible<A, B>

Lazy (Python call time)

No

py::arg("x") = default_value

Immediate (registration time)

Yes – see The Bridge Pattern Trap

Note

This lazy resolution applies to the runtime type registry used by pybind11’s type casters when functions are called from Python. It is distinct from the compile-time type caster issue described in The Cross-Translation-Unit Holder Problem, where template instantiation across translation units requires the py::cast() workaround.

Convenience Macros

// Declare a module binding function
DECLARE_MODULE_BINDINGS(time_bindings);

// Add binding to base submodule
ADD_BASE_BINDING(ql_patterns::observable, "Observable");

// Add binding to main module
ADD_MAIN_BINDING(ql_time::date, "Date");

Module Patterns

Each QuantLib domain maps to a source directory with a consistent structure.

Directory Pattern

src/quotes/
├── all.cpp              # Module aggregator
├── simplequote.cpp      # Individual class binding
├── derivedquote.cpp
└── compositequote.cpp

Aggregator Pattern (all.cpp)

#include "pyquantlib/pyquantlib.h"
#include "pyquantlib/binding_manager.h"

DECLARE_MODULE_BINDINGS(quotes_bindings) {
    auto m = manager.module();

    manager.addFunction(ql_quotes::simplequote, m, "SimpleQuote");
    manager.addFunction(ql_quotes::derivedquote, m, "DerivedQuote");
    manager.addFunction(ql_quotes::compositequote, m, "CompositeQuote");
}

Individual Binding Pattern

// simplequote.cpp
#include <ql/quotes/simplequote.hpp>
#include <pybind11/pybind11.h>

namespace py = pybind11;

namespace ql_quotes {

void simplequote(py::module_& m) {
    py::class_<QuantLib::SimpleQuote,
               QuantLib::Quote,
               QuantLib::ext::shared_ptr<QuantLib::SimpleQuote>>(
        m, "SimpleQuote", "Quote with a settable value.")
        .def(py::init<QuantLib::Real>(),
             py::arg("value") = 0.0,
             "Creates a SimpleQuote with the given value.")
        .def("setValue", &QuantLib::SimpleQuote::setValue,
             py::arg("value"),
             "Sets the quote value and notifies observers.");
}

}  // namespace ql_quotes

Forward Declarations (pyquantlib.h)

All binding functions are declared in a central header:

namespace ql_quotes {
    void simplequote(py::module_&);
    void derivedquote(py::module_&);
    void compositequote(py::module_&);
}

namespace ql_time {
    void date(py::module_&);
    void calendar(py::module_&);
    // ...
}

Trampoline Classes

Trampoline classes enable Python code to subclass QuantLib abstract base classes. See Extending PyQuantLib for the user-facing documentation.

Implementation

pybind11 trampolines intercept virtual method calls and redirect them to Python:

class PyQuote : public QuantLib::Quote {
public:
    using QuantLib::Quote::Quote;

    QuantLib::Real value() const override {
        PYBIND11_OVERRIDE_PURE(
            QuantLib::Real,      // Return type
            QuantLib::Quote,     // Parent class
            value,               // Method name
        );
    }

    bool isValid() const override {
        PYBIND11_OVERRIDE_PURE(
            bool,
            QuantLib::Quote,
            isValid,
        );
    }
};

Binding with Trampoline

Most trampolines use the standard py::class_ pattern:

py::class_<QuantLib::Quote,
           PyQuote,                              // Trampoline class
           QuantLib::ext::shared_ptr<QuantLib::Quote>,
           QuantLib::Observable>(
    m, "Quote", "Abstract base class for market quotes.")
    .def(py::init_alias<>());                   // Enables Python subclassing

Diamond Inheritance: py::classh and trampoline_self_life_support

Some classes require py::classh (smart_holder) instead of py::class_, and their trampolines may need to inherit from py::trampoline_self_life_support. See The Diamond Inheritance Problem for when this applies and why.

// SmileSection inherits from both Observable and Observer (diamond) -- uses py::classh
py::classh<SmileSection, PySmileSection,
           Observer, Observable>(m, "SmileSection", "...")

// SabrInterpolatedSmileSection closes the diamond via SmileSection + LazyObject
py::classh<SabrInterpolatedSmileSection, SmileSection, LazyObject>(
    m, "SabrInterpolatedSmileSection", "...")

Guidelines for Contributors

  1. Only virtual methods: Non-virtual methods cannot be overridden from Python. C++ calls bypass the trampoline and go directly to the base class. Including non-virtual methods gives the false impression they are overridable.

  2. Use override: If it doesn’t compile with override, the method isn’t virtual: remove it from the trampoline

  3. PYBIND11_OVERRIDE_PURE vs PYBIND11_OVERRIDE: Use PYBIND11_OVERRIDE_PURE for pure virtual methods (= 0), which throws if not implemented in Python. Use PYBIND11_OVERRIDE for virtual methods with a base implementation, which falls back to C++ if not overridden.

  4. Trailing comma: PYBIND11_OVERRIDE macros need trailing comma for zero-arg methods (C++20 compatibility)

  5. Diamond inheritance: See the The Diamond Inheritance Problem design notes for when py::classh and trampoline_self_life_support are required.

All trampolines are in include/pyquantlib/trampolines.h.

Handle Templates

QuantLib uses Handle<T> and RelinkableHandle<T> extensively. PyQuantLib provides helper templates for binding these.

bindHandle Template

template <typename T>
auto bindHandle(py::module_& m,
                const std::string& class_name,
                const std::string& doc_string = "") {
    using HandleType = QuantLib::Handle<T>;

    return py::class_<HandleType>(m, class_name.c_str(), doc_string.c_str())
        .def(py::init<>(), "Creates an empty handle.")
        .def(py::init<const QuantLib::ext::shared_ptr<T>&, bool>(),
             py::arg("ptr"), py::arg("registerAsObserver") = true)
        .def("empty", &HandleType::empty)
        .def("__bool__", [](const HandleType& h) { return !h.empty(); })
        .def("currentLink", &HandleType::currentLink)
        .def(py::self == py::self)
        .def(py::self != py::self);
}

bindRelinkableHandle Template

template <typename T>
auto bindRelinkableHandle(py::module_& m,
                          const std::string& class_name,
                          const std::string& doc_string = "") {
    using RelinkableHandleType = QuantLib::RelinkableHandle<T>;
    using HandleType = QuantLib::Handle<T>;

    return py::class_<RelinkableHandleType, HandleType>(m, class_name.c_str())
        .def(py::init<>())
        .def(py::init<const QuantLib::ext::shared_ptr<T>&, bool>(),
             py::arg("ptr"), py::arg("registerAsObserver") = true)
        .def("linkTo", &RelinkableHandleType::linkTo,
             py::arg("ptr"), py::arg("registerAsObserver") = true);
}

Usage

// In yieldtermstructurehandle.cpp
bindHandle<QuantLib::YieldTermStructure>(
    m, "YieldTermStructureHandle",
    "Handle to a yield term structure.");

// In relinkableyieldtermstructurehandle.cpp
bindRelinkableHandle<QuantLib::YieldTermStructure>(
    m, "RelinkableYieldTermStructureHandle",
    "Relinkable handle to a yield term structure.");

Implicit Conversion

PyQuantLib uses py::implicitly_convertible to enable automatic conversion from Python types to QuantLib types.

Type

Converts From

Defined In

Date

datetime.date, datetime.datetime

date.cpp

Array

lists, numpy arrays

array.cpp

Matrix

list of lists, 2D numpy arrays

matrix.cpp

At the end of each binding function, the conversion is registered:

py::implicitly_convertible<py::object, QuantLib::Date>();
py::implicitly_convertible<py::list, Array>();
py::implicitly_convertible<py::array, Array>();

Date Arithmetic with datetime.date

Period defines __radd__ and __rsub__ so that datetime.date objects can participate in date arithmetic:

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)

When Python evaluates datetime.date + ql.Period, datetime.date.__add__ returns NotImplemented, and Python falls back to Period.__radd__, which converts the date to ql.Date via the implicit conversion and returns the result. Both datetime.date and datetime.datetime are supported. The return type is always ql.Date.