Contributing¶
Contributions to PyQuantLib are welcome!
Getting Started¶
For setup instructions, see Building from Source.
git clone https://github.com/quantales/pyquantlib.git
cd pyquantlib
python -m venv venv
source venv/bin/activate
pip install -r requirements-dev.txt
pip install -e .
pytest
Development Workflow¶
Running Tests¶
pytest # All tests
pytest --cov=pyquantlib # With coverage
pytest tests/test_date.py # Specific file
pytest -v # Verbose
Linting¶
ruff check tests/ pyquantlib/
Adding or Updating Bindings¶
Workflow¶
When adding or modifying bindings, follow these steps:
C++ bindings: Create/update
.cppfile insrc/Tests: Add tests in
tests/Build and verify:
pip install -e . && pytestAPI docs: Update examples in
docs/api/if neededStubs: Maintainer regenerates (contributors skip this step)
File Checklist¶
Add/update
.cppfile in appropriatesrc/subdirectoryDeclare binding function in
include/pyquantlib/pyquantlib.hRegister in module’s
all.cppConsider hidden handle constructors for classes using handles
Add tests in
tests/Update API docs examples (if user-facing API changed)
API Documentation¶
API docs in docs/api/ use a consistent 3-level heading structure:
# Module Name (h1)
## Group Name (h2) - mirrors QuantLib subdirectory
### ClassName (h3) - one per class
Each class entry uses Sphinx autoclass to pull documentation from C++ docstrings:
### NewClassName
Brief description (one line).
```{eval-rst}
.. autoclass:: pyquantlib.NewClassName
:members:
:undoc-members:
```
```python
# Optional usage example
engine = ql.NewClassName(process)
```
Part |
Source |
|---|---|
|
Automated from C++ docstrings |
Brief description |
Manual (one line) |
Usage examples |
Manual (optional) |
For new classes, the minimum required is the autoclass directive with a brief description.
Example¶
// src/quotes/simplequote.cpp
#include "pyquantlib/pyquantlib.h"
#include <ql/quotes/simplequote.hpp>
#include <pybind11/pybind11.h>
namespace py = pybind11;
using namespace QuantLib;
void ql_quotes::simplequote(py::module_& m) {
py::class_<SimpleQuote, Quote, ext::shared_ptr<SimpleQuote>>(m, "SimpleQuote",
"Simple quote for market data.")
.def(py::init<Real>(), py::arg("value"))
.def("setValue", &SimpleQuote::setValue, py::arg("value"))
.def("reset", &SimpleQuote::reset);
}
Code Conventions¶
File Headers¶
Each file lists its contributors. When adding or substantially modifying a file, add the appropriate copyright line.
Includes¶
// GOOD: Specific headers
#include <ql/quotes/simplequote.hpp>
#include <ql/time/date.hpp>
// BAD: Avoid umbrella header
#include <ql/quantlib.hpp>
Docstrings¶
Docstrings in pybind11 bindings are automatically pulled into the API documentation via Sphinx autoclass. Keep them concise:
// GOOD
.def("value", &Quote::value, "Returns the current value.")
// AVOID
.def("value", &Quote::value, "This method returns the current value of the quote object.")
Class-level docstrings appear in the API reference, so write them as brief descriptions:
py::class_<SimpleQuote, Quote, ext::shared_ptr<SimpleQuote>>(
m, "SimpleQuote", "Simple quote with settable value.")
Common Pitfalls¶
Bridge-Pattern Classes¶
QuantLib’s DayCounter and Calendar use the bridge (pimpl) pattern. Their default constructors create objects with no implementation (null internal pointer). These “empty” objects throw errors when used, e.g., “no day counter implementation provided”.
In pybind11 bindings, default argument values are evaluated at module import time. If a binding uses DayCounter() as a default, the invalid object is created during import, which can cause import failures if any code path touches it.
There are three fixes, depending on the situation:
// BAD: Causes import failure
py::arg("dayCounter") = DayCounter()
// FIX 1: Concrete default (when any valid value works)
py::arg("dayCounter") = Actual365Fixed()
// FIX 2: Required argument (when no sensible default exists)
py::arg("dayCounter")
// FIX 3: py::none() sentinel (when DayCounter() carries semantic meaning)
// Used when the null default means "use the index's day counter" or similar
.def(py::init([](/* ... */, const py::object& dayCounter, /* ... */) {
DayCounter dc;
if (!dayCounter.is_none())
dc = dayCounter.cast<DayCounter>();
// ... pass dc to C++ constructor
}), py::arg("dayCounter") = py::none())
Situation |
Fix |
|---|---|
Default is arbitrary (any valid value works) |
Replace with a concrete default |
No sensible default exists in C++ |
Make the parameter required |
Default is a sentinel with semantic meaning |
Use |
Most bindings fall into the first category. The third applies when the C++ null default triggers fallback behavior (e.g., FloatingRateCoupon using the index’s day counter, or Null<Natural>() for optional lookback days). Getting this wrong produces silent, incorrect results.
See The Bridge Pattern Trap for the full story.
Note
The same py::none() + lambda pattern works for Null<T>() sentinels, which pybind11 also cannot convert as default arguments.
Enum Pass-by-Reference¶
pybind11 enum values are singletons. Passing them by reference to C++ functions that modify them corrupts the singleton for all subsequent uses in the Python session.
// BAD: corrupts enum singleton
.def("check", [](const Foo& self, SomeEnum& e) {
return self.check(e); // e modified in place → singleton corrupted
})
// GOOD: pass by value, return tuple with modified value
.def("check", [](const Foo& self, SomeEnum e) {
bool result = self.check(e);
return py::make_tuple(result, e);
})
Note
This behavior is not explicitly documented in pybind11, but is a consequence of how py::enum_ implements singletons internally. pybind11 v3 introduced py::native_enum which uses Python’s native enum module and is recommended for new bindings. This may behave differently but has not been tested in PyQuantLib.
See The Enum Singleton Problem for the full story of how this issue was discovered and debugged.
Trampoline Classes¶
See Internals for trampoline implementation details and guidelines.
Error Handling in Tests¶
PyQuantLib raises two types of exceptions:
Exception |
Source |
When |
|---|---|---|
|
PyQuantLib wrapper |
Validation in the binding code (e.g., invalid RNG type) |
|
QuantLib internals |
QuantLib’s own error checks (e.g., invalid parameters) |
# RuntimeError: PyQuantLib wrapper validates input
with pytest.raises(RuntimeError, match="Unsupported RNG type"):
ql.MCEuropeanEngine(process, "invalid_rng", ...)
# ql.Error: QuantLib validates internally
with pytest.raises(ql.Error, match="two underlyings"):
ql.StulzEngine(process_array) # requires exactly 2 processes
Use RuntimeError for errors thrown in the wrapper code (std::runtime_error) and ql.Error for errors originating from QuantLib.
Type Stubs¶
PyQuantLib includes .pyi stub files for IDE autocompletion and type checking. These are generated using pybind11-stubgen.
pybind11-stubgen produces non-deterministic import ordering across platforms. The same bindings on Windows vs Linux generate identical stubs but with imports in different order:
# Windows might produce:
from pyquantlib._pyquantlib import GeneralizedBlackScholesProcess as BlackScholesMertonProcess
from pyquantlib._pyquantlib import GeneralizedBlackScholesProcess
# Linux might produce:
from pyquantlib._pyquantlib import GeneralizedBlackScholesProcess
from pyquantlib._pyquantlib import GeneralizedBlackScholesProcess as BlackScholesMertonProcess
This causes spurious diffs and merge conflicts. To avoid this, the maintainer regenerates stubs on a single platform (Windows) after merging PRs.
Contributors can regenerate stubs locally, but should not include regenerated stubs in pull requests.
python scripts/stubgen.py
Note
Stub validation is not included in CI due to the cross-platform non-determinism described above.
Questions?¶
Open an issue on GitHub.