| name | add-python-binding |
| description | Use when adding new Python bindings (exposing C++ classes/functions to Python) in the DAWorkBench project, or modifying existing bindings. Trigger words: python binding, pybind11 binding, expose to python, add binding, export python module, bind C++ to Python. |
Add Python Binding
Overview
This skill guides adding Python bindings for C++ code in the DAWorkBench project. The project uses pybind11 embedded mode (PYBIND11_EMBEDDED_MODULE) to expose C++ classes and functions to the Python scripting environment. Bindings fall into four scenarios, each with distinct patterns and key points.
Decision Tree: What Are You Binding?
What are you binding?
├── A standalone free function or singleton accessor → Scenario A: Simple Function Binding
├── A Qt-interfacing class (with QString/QList etc. parameters) → Scenario B: Qt-Interfacing Class Binding
├── A data wrapper class (pandas/numpy object wrapper) → Scenario C: Data Wrapper Binding
└── C++ needs to call Python functions → Scenario D: Python Module Import
!!!danger slots workaround (First Step for All Scenarios)
Before including any pybind11 header, you must first include DAPybind11InQt.h. Qt's slots macro conflicts with pybind11. This header handles the #undef slots → include pybind11 → #define slots Q_SLOTS toggle.
#include "DAPybind11InQt.h"
Scenario A: Simple Function Binding
Use for: Standalone free functions, singleton accessors (no Qt type parameters).
Skeleton
#ifndef MYPYTHONBINDING_H
#define MYPYTHONBINDING_H
#include <string>
namespace DA {
void myHelperFunction(const std::string& msg);
DA::DACoreInterface* getMyCorePtr();
}
#endif
#include "MyPythonBinding.h"
#include "DAPybind11InQt.h"
#include "DACoreInterface.h"
namespace DA {
void myHelperFunction(const std::string& msg) {
qInfo() << QString::fromStdString(msg);
}
}
PYBIND11_EMBEDDED_MODULE(da_my_module, m)
{
m.def("getCore",
&DA::getMyCorePtr,
"Return the core interface (singleton)",
pybind11::return_value_policy::reference);
m.def("myHelper", &DA::myHelperFunction, "A helper function");
}
Key Steps
#include "DAPybind11InQt.h" as the first pybind11-related header
- Define module with
PYBIND11_EMBEDDED_MODULE(module_name, m)
.def() for free functions
- Singleton returns must specify
pybind11::return_value_policy::reference, otherwise pybind11 will try to destruct it
- Module names follow the
da_xxx prefix convention
Reference Files
| File | Purpose |
|---|
src/APP/PythonBinding/DAAppPythonBinding.cpp | Full Scenario A example |
src/APP/PythonBinding/DAAppPythonBinding.h | Function declaration template |
src/DAPyBindQt/DAPybind11InQt.h | slots workaround |
Scenario B: Qt-Interfacing Class Binding
Use for: Interface classes with QString, QList, QJsonObject and other Qt type parameters.
Skeleton
#include "DAPybind11InQt.h"
#include "DAPybind11QtCaster.hpp"
#include "DAPythonSignalHandler.h"
#include "MyQtInterface.h"
PYBIND11_EMBEDDED_MODULE(da_my_interface, m)
{
pybind11::class_<DA::MyQtInterface>(m, "MyQtInterface")
.def("showMessage",
[](DA::MyQtInterface& self, const std::string& message, int timeout) {
self.showMessage(QString::fromStdString(message), timeout);
},
pybind11::arg("message"),
pybind11::arg("timeout") = 15000)
.def("getSubInterface",
&DA::MyQtInterface::getSubInterface,
pybind11::return_value_policy::reference_internal)
.def("getAllItems",
[](DA::MyQtInterface& self) {
QList<DA::MyItem> items = self.getAllItems();
pybind11::list pyList;
for (const DA::MyItem& item : items) {
pyList.append(item);
}
return pyList;
},
"Get all items as a Python list")
.def("getConfigValues",
[](DA::MyQtInterface& self, const std::string& jsonConfig, const std::string& cacheKey) {
QJsonObject jsonObj = self.getConfigValues(
QString::fromStdString(jsonConfig),
self.getMainWindow(),
QString::fromStdString(cacheKey));
return DA::PY::qjsonObjectToPyDict(jsonObj);
},
pybind11::arg("jsonConfig"),
pybind11::arg("cacheKey") = "")
;
pybind11::class_<DA::DAPythonSignalHandler>(m, "DAPythonSignalHandler")
.def(pybind11::init<>())
.def("callInMainThread",
[](DA::DAPythonSignalHandler& self, pybind11::function pyFunc) {
self.callInMainThread([pyFunc]() {
try {
pybind11::gil_scoped_acquire acquire;
pyFunc();
} catch (const pybind11::error_already_set& e) {
qCritical() << "Python error in main thread callback:" << e.what();
}
});
},
pybind11::arg("func"));
}
Key Steps
#include "DAPybind11InQt.h" as the first pybind11-related header (!!!danger mandatory)
#include "DAPybind11QtCaster.hpp" to bring in Qt type casters
pybind11::class_<>() to bind the interface class
QString parameters: lambda wrapper, receive std::string then convert with QString::fromStdString()
- Return internal objects:
pybind11::return_value_policy::reference_internal (lifetime bound to parent object)
QList<T> returns: lambda manually iterates and converts to pybind11::list (cannot rely on automatic QList caster)
getConfigValues: std::string JSON → QString → dialog → QJsonObject → DA::PY::qjsonObjectToPyDict() to pybind11::dict
- Cross-thread callbacks:
DAPythonSignalHandler::callInMainThread + pybind11::gil_scoped_acquire
Reference Files
| File | Purpose |
|---|
src/DAInterface/DAInterfacePythonBinding.cpp | Full Scenario B example |
src/DAPyBindQt/DAPybind11QtCaster.hpp | Qt type casters |
src/DAPyBindQt/DAPythonSignalHandler.h | Cross-thread communication |
src/DAPyBindQt/DAPyJsonCast.h | QJsonObject ↔ pybind11::dict |
Scenario C: Data Wrapper Binding
Use for: C++ classes that wrap pandas/numpy Python objects (e.g. DAData, DAPyDataFrame).
Skeleton
#ifndef MYDATAPYTHONBINDING_H
#define MYDATAPYTHONBINDING_H
#include "DAPybind11InQt.h"
#include "MyData.h"
DA::DAPyDataFrame pyDataFrameToDAPyDataFrame(pybind11::object df);
void addDataFrameFromPy(DA::MyDataManager& mgr, pybind11::object df, const std::string& name);
#endif
#include "MyDataPythonBinding.h"
#include "DAPybind11InQt.h"
#include "MyData.h"
DA::DAPyDataFrame pyDataFrameToDAPyDataFrame(pybind11::object df)
{
return DA::DAPyDataFrame(df);
}
void addDataFrameFromPy(DA::MyDataManager& mgr, pybind11::object df, const std::string& name)
{
DA::DAPyDataFrame daDf = pyDataFrameToDAPyDataFrame(df);
DA::MyData data(daDf);
data.setName(QString::fromStdString(name));
mgr.addData(data);
}
PYBIND11_EMBEDDED_MODULE(da_my_data, m)
{
pybind11::class_<DA::MyData>(m, "MyData")
.def(pybind11::init<>())
.def(pybind11::init<pybind11::object>())
.def("toPyObject", &DA::MyData::toPyObject, "Return the underlying pandas object")
.def("getName", [](const DA::MyData& self) { return self.getName().toStdString(); })
.def("setName", [](DA::MyData& self, const std::string& n) { self.setName(QString::fromStdString(n)); })
.def("isNull", &DA::MyData::isNull, "Check if the data is null")
.def("id", &DA::MyData::id, "Return the data id");
pybind11::enum_<DA::MyDataManager::ChangeType>(m, "DataChangeType")
.value("Name", DA::MyDataManager::ChangeName)
.value("Value", DA::MyDataManager::ChangeValue)
.export_values();
pybind11::class_<DA::MyDataManager>(m, "MyDataManager")
.def("addDataFrame", &addDataFrameFromPy, "Add a pandas DataFrame to manager")
.def("addData",
static_cast<void(DA::MyDataManager::*)(DA::MyData&)>(&DA::MyDataManager::addData),
"Add a MyData object to manager")
.def("getDataCount", &DA::MyDataManager::getDataCount)
.def("getAllDatas",
[](DA::MyDataManager& self) {
QList<DA::MyData> datas = self.getAllDatas();
pybind11::list pyList;
for (const DA::MyData& data : datas) { pyList.append(data); }
return pyList;
});
}
Key Steps
#include "DAPybind11InQt.h" as the first pybind11-related header
pybind11::init<pybind11::object>() constructor accepts pandas objects
QString properties: lambda wrapper with std::string ↔ QString conversion
pybind11::enum_<>() + .value() + .export_values() to export enums
QList<T> returns: lambda manually converts to pybind11::list
- Overloaded functions: use
static_cast<function_signature>(&Class::method) to select the correct overload
- Recommended: Write a complete doxygen API reference in the
.h file (type table + construction notes + member docs + examples + limitations)
Reference Files
| File | Purpose |
|---|
src/DAData/DADataPythonBinding.cpp | Full Scenario C example |
src/DAData/DADataPythonBinding.h | .h doxygen API reference example (143 lines, best practice) |
Scenario D: Python Module Import
Use for: C++ needs to call Python functions (e.g. numpy/pandas APIs), not exposing C++ to Python.
Skeleton
#ifndef DAPYMODULEMYLIB_H
#define DAPYMODULEMYLIB_H
#include "DAPyBindQtGlobal.h"
#include "DAPyModule.h"
namespace DA {
class DAPYBINDQT_API DAPyModuleMyLib : public DAPyModule
{
DA_DECLARE_PRIVATE(DAPyModuleMyLib)
DAPyModuleMyLib();
public:
~DAPyModuleMyLib();
static DAPyModuleMyLib& getInstance();
void finalize();
bool import();
bool isInstanceMyType(const pybind11::object& obj) const;
private:
};
}
#endif
#include "DAPyModuleMyLib.h"
#include "DAPybind11InQt.h"
#include <QDebug>
namespace DA {
class DAPyModuleMyLib::PrivateData
{
DA_DECLARE_PUBLIC(DAPyModuleMyLib)
public:
PrivateData(DAPyModuleMyLib* p);
QString mLastErrorString;
pybind11::object mObjMyType;
pybind11::object mObjMyFunc;
};
DAPyModuleMyLib::PrivateData::PrivateData(DAPyModuleMyLib* p) : q_ptr(p) {}
DAPyModuleMyLib::DAPyModuleMyLib() : DAPyModule(), DA_PIMPL_CONSTRUCT
{
import();
try {
d_ptr->mObjMyType = attr("MyType");
d_ptr->mObjMyFunc = attr("my_function");
} catch (const std::exception& e) {
d_ptr->mLastErrorString = e.what();
}
}
bool DAPyModuleMyLib::import()
{
return DAPyModule::import("my_lib");
}
bool DAPyModuleMyLib::isInstanceMyType(const pybind11::object& obj) const
{
return pybind11::isinstance(obj, d_ptr->mObjMyType);
}
DAPyModuleMyLib& DAPyModuleMyLib::getInstance()
{
static DAPyModuleMyLib s_instance;
return s_instance;
}
}
Key Steps
- Inherit
DAPyModule (which inherits DAPyObjectWrapper, not QObject)
- Use PIMPL pattern (
DA_DECLARE_PRIVATE / DA_PIMPL_CONSTRUCT)
- Call
import() in constructor → DAPyModule::import("module_name") to import the Python module
- Use
attr("name") to get Python object references, cache them in PrivateData (avoids repeated lookup overhead)
- Use
pybind11::isinstance(obj, cached_type) for type checking
- Singleton pattern:
getInstance() returns a static instance
Reference Files
| File | Purpose |
|---|
src/DAPyBindQt/numpy/DAPyModuleNumpy.h | Scenario D header template |
src/DAPyBindQt/numpy/DAPyModuleNumpy.cpp | Scenario D full implementation example |
src/DAPyBindQt/DAPyModule.h | DAPyModule base class API |
Key Differences
| Aspect | Scenario A | Scenario B | Scenario C | Scenario D |
|---|
| Base pattern | PYBIND11_EMBEDDED_MODULE + .def() | PYBIND11_EMBEDDED_MODULE + class_<>() | PYBIND11_EMBEDDED_MODULE + class_<>() + enum_<>() | Inherit DAPyModule, no PYBIND11_EMBEDDED_MODULE |
| Qt type handling | None (pure std::string) | Lambda wrapping QString / manual QList conversion | Lambda wrapping QString | None (C++ calls Python, no Qt types exposed) |
| Ownership policy | reference (singleton) | reference_internal (child bound to parent lifetime) | Default (wrapper manages itself) | N/A (C++ side holds pybind11::object) |
| Enum export | Not applicable | Not applicable | pybind11::enum_<>() + .export_values() | Not applicable |
| .h doxygen API reference | Concise function declarations | Can be omitted (see DAInterfacePythonBinding.h, only 4 lines) | Recommended (see DADataPythonBinding.h, 143 lines) | Not applicable (PIMPL class) |
| Stub update | Add da_my_module.pyi | Add da_my_interface.pyi | Add da_my_data.pyi | Not applicable |
Post-Binding Checklist
After adding a new Python binding module, these files must be updated synchronously:
- ✅ stubs/ — Add a
.pyi file following existing stub format (Chinese docstring + type annotations + cross-module imports). Watch for circular import edge cases: if the new module is referenced by da_interface, update da_interface/__init__.pyi imports too
- ✅ stubs/mock/ — Add a mock
.py file following existing mock format. Note lazy-loading pattern: see stubs/mock/da_interface.py for avoiding circular dependencies
- ✅ docs/zh/dev-guide/python-in-cpp.md — Update section 9 roadmap table, add row for the new module
- ✅ docs/zh/dev-guide/embedded-python-debugging.md — Update section 2 module overview table, add row for the new module
- ✅ CMakeLists.txt — Add new binding files in the
DA_ENABLE_PYTHON conditional block
- ✅ mkdocs.yml — If navigation structure needs updating (new module docs page)
Compatibility Notes
Qt5 / Qt6
- slots workaround:
DAPybind11InQt.h handles #undef slots → include pybind11 → #define slots Q_SLOTS. All binding files must include this header first
QVector only exists in Qt5; in Qt6 QVector = QList. Use version-check macros if needed
QButtonGroup::buttonClicked in Qt5 uses QOverload<int>::of(), Qt6 uses idClicked
DA_ENABLE_PYTHON Conditional Compilation
All binding code must be guarded with #ifdef DA_ENABLE_PYTHON. In CMakeLists.txt, binding files are only compiled when DA_ENABLE_PYTHON=ON.
PIMPL Classes
Only bind public methods. Never attempt to bind DA_D pointers or PrivateData members. PIMPL private data must not be exposed to Python.
QwtPlotItem Subclasses
QwtPlotItem-related classes do not inherit QObject, so Q_OBJECT macro is not allowed. Do not use dynamic properties when binding them.
QList
Requires lambda manual conversion to pybind11::list (iterate and append each item). Cannot rely on automatic QList caster because bound types may not have registered one.
QVariant caster
DAPybind11QtCaster.hpp provides a QVariant caster supporting numpy object conversion, but not all Qt type combinations can serve as QHash keys.
Reference Files
| File | Path | Purpose |
|---|
| DAAppPythonBinding.cpp | src/APP/PythonBinding/ | Scenario A reference |
| DAAppPythonBinding.h | src/APP/PythonBinding/ | Scenario A function declaration template |
| DAInterfacePythonBinding.cpp | src/DAInterface/ | Scenario B reference (Qt-interfacing class + lambda wrapping + QList conversion) |
| DADataPythonBinding.cpp | src/DAData/ | Scenario C reference (data wrapper class + enum export) |
| DADataPythonBinding.h | src/DAData/ | .h doxygen API reference example (143 lines) |
| DAPyModuleNumpy.h/.cpp | src/DAPyBindQt/numpy/ | Scenario D reference (module import + lazy caching) |
| DAPyModule.h | src/DAPyBindQt/ | DAPyModule base class API |
| DAPybind11InQt.h | src/DAPyBindQt/ | slots workaround (mandatory for all scenarios) |
| DAPybind11QtCaster.hpp | src/DAPyBindQt/ | Qt type casters |
| DAPyInterpreter.h | src/DAPyBindQt/ | Python interpreter management |
| DAPythonSignalHandler.h | src/DAPyBindQt/ | Cross-thread communication |
| stubs/da_interface/init.pyi | stubs/da_interface/ | Stub format reference |
| stubs/mock/da_interface.py | stubs/mock/ | Mock format reference (lazy-loading pattern) |