2025, Nov 25 13:00
Embedding CPython in a C++ DLL on Windows: how to package Python correctly and avoid encodings errors
Learn how to embed CPython in a C++ DLL on Windows, fix ModuleNotFoundError: encodings, set PYTHONHOME, and package python311.dll with the standard library.
Embedding CPython inside a C++ DLL on Windows looks straightforward until the build works only on the development machine and falls apart elsewhere. A classic symptom is ModuleNotFoundError: No module named 'encodings' when you move the binaries away from the solution directory. The immediate cause is not the API calls but the runtime layout and how CPython discovers its standard library. Below is a practical walkthrough of why this happens and how to package Python so the DLL runs anywhere without relying on vcpkg_installed.
Minimal embedding that appears to work in the build tree
The following snippet initializes the interpreter and calls into Python. It succeeds near the development layout because it hardcodes paths derived from vcpkg_installed.
void ensurePyBootstrapped() {
if (!Py_IsInitialized()) {
PyConfig cfg;
PyConfig_InitPythonConfig(&cfg);
wchar_t modPath[MAX_PATH];
GetModuleFileNameW(NULL, modPath, MAX_PATH);
std::wstring modDir = modPath;
size_t offs = modDir.find_last_of(L"\\");
if (offs != std::wstring::npos) {
modDir = modDir.substr(0, offs);
}
std::wstring homeDir = modDir + L"\\..\\..\\vcpkg_installed\\x64-windows-static-md\\x64-windows-static-md\\tools\\python3";
wchar_t absBuf[MAX_PATH];
GetFullPathNameW(homeDir.c_str(), MAX_PATH, absBuf, NULL);
homeDir = absBuf;
std::wstring libDir = homeDir + L"\\Lib";
std::wstring sitePkgs = libDir + L"\\site-packages";
std::wstring dllDir = homeDir + L"\\DLLs";
PyConfig_SetString(&cfg, &cfg.home, homeDir.c_str());
std::wstring searchPath = libDir + L";" + sitePkgs + L";" + dllDir;
PyConfig_SetString(&cfg, &cfg.pythonpath_env, searchPath.c_str());
PyStatus st = Py_InitializeFromConfig(&cfg);
PyConfig_Clear(&cfg);
if (PyStatus_Exception(st)) {
PyErr_Print();
return;
}
PyRun_SimpleString("import sys");
PyRun_SimpleString("sys.path.append(\".\")");
}
}
void EXPORTED_API pyEcho(const char* msg) {
ensurePyBootstrapped();
PyObject* s = PyUnicode_FromString(msg);
if (s) {
PyObject* builtins = PyEval_GetBuiltins();
PyObject* printFn = PyDict_GetItemString(builtins, "print");
if (printFn && PyCallable_Check(printFn)) {
PyObject* args = PyTuple_Pack(1, s);
PyObject_CallObject(printFn, args);
Py_DECREF(args);
}
Py_DECREF(s);
}
}
Client application:
#include <iostream>
#include "py_embed.h"
int main() {
std::cout << "Hello";
pyEcho("Hello from Python");
}
What goes wrong and why
CPython does not become self-contained just because you ship python3.dll and python312.dll. The interpreter requires the standard library to be present at PYTHONHOME (which you can override via the configuration APIs). When you run near vcpkg_installed, Lib, Lib\site-packages and DLLs are discoverable, so imports like encodings resolve. Move the binaries elsewhere and those directories disappear, so the interpreter starts but immediately fails to import encodings. The runtime printout showing sys.path and the failure is a direct consequence of pointing PYTHONHOME at a location that only exists inside the build layout.
There is another practical constraint to keep in mind on Windows. The process loader must be able to locate python311.dll or python312.dll at load time. Keeping the Python DLL next to your DLL satisfies the loader. If you place the Python DLL in a subdirectory, you must set PATH to that location before load or load it explicitly with LoadLibrary instead of linking it directly.
Make the DLL independent from vcpkg_installed
The reliable way is to ship the interpreter with your application and initialize CPython against that packaged runtime. Set PYTHONHOME before calling Py_InitializeFromConfig and point it to a python folder you distribute alongside your binaries. This is how popular desktop applications solve the problem.
.
└── app_root/
├── app.exe
├── python311.dll
└── python/
├── DLLs/
│ ├── _ctypes.pyd
│ └── ...
├── Lib/
│ ├── site_packages/
│ │ └── numpy, etc...
│ ├── os.py
│ └── ...
└── bin (optional)/
├── python311.dll (see note below)
└── python.exe (see note below)
If you place python311.dll in a subdirectory rather than next to the executable, adjust PATH accordingly or load it with LoadLibrary and avoid direct linking. Packaging python.exe with your app enables users to install extra libraries using pip by invoking python.exe -m pip install ...
Corrected initialization pointing to your packaged Python
The following initialization uses the executable directory as the anchor and sets PYTHONHOME to app_root\python. The rest of the logic remains the same, but the hardwired dependency on the vcpkg_installed tree is gone.
void initEmbeddedPython() {
if (!Py_IsInitialized()) {
PyConfig cfg;
PyConfig_InitPythonConfig(&cfg);
wchar_t exeBuf[MAX_PATH];
GetModuleFileNameW(NULL, exeBuf, MAX_PATH);
std::wstring baseDir = exeBuf;
size_t cut = baseDir.find_last_of(L"\\");
if (cut != std::wstring::npos) {
baseDir = baseDir.substr(0, cut);
}
std::wstring pkgHome = baseDir + L"\\python";
std::wstring libDir = pkgHome + L"\\Lib";
std::wstring sitePkgs = libDir + L"\\site-packages";
std::wstring dllDir = pkgHome + L"\\DLLs";
PyConfig_SetString(&cfg, &cfg.home, pkgHome.c_str());
std::wstring searchPath = libDir + L";" + sitePkgs + L";" + dllDir;
PyConfig_SetString(&cfg, &cfg.pythonpath_env, searchPath.c_str());
PyStatus st = Py_InitializeFromConfig(&cfg);
PyConfig_Clear(&cfg);
if (PyStatus_Exception(st)) {
PyErr_Print();
return;
}
PyRun_SimpleString("import sys");
PyRun_SimpleString("sys.path.append(\".\")");
}
}
void EXPORTED_API pyEcho(const char* msg) {
initEmbeddedPython();
PyObject* s = PyUnicode_FromString(msg);
if (s) {
PyObject* builtins = PyEval_GetBuiltins();
PyObject* printFn = PyDict_GetItemString(builtins, "print");
if (printFn && PyCallable_Check(printFn)) {
PyObject* args = PyTuple_Pack(1, s);
PyObject_CallObject(printFn, args);
Py_DECREF(args);
}
Py_DECREF(s);
}
}
With this layout, the interpreter resolves encodings and the rest of the standard library from the packaged python folder, independent of the build system. The Python DLL should be next to your executable or DLL to satisfy the Windows loader unless you take the alternative routing through PATH or LoadLibrary.
When a full CPython is overkill
If you only need to parse or evaluate Python syntax for lightweight scripting and do not need C or C++ extension modules like numpy, consider other implementations such as RustPython, IronPython, JYThon, or pocketpy. They can be embedded without bundling the CPython standard library, but they do not support CPython’s C extension ecosystem.
Security trade-offs you should be aware of
Packaging Python means the standard library and your scripts live on disk next to the application. Users can read and modify them. This makes secret-keeping impossible and opens the door for code injection through local modification. One way to reduce tampering is to use a custom importer that loads code from memory rather than from files on disk, though native extension modules complicate that approach. Even then, determined attackers can reverse engineer the result.
Why this knowledge matters
Without the standard library at PYTHONHOME, the interpreter fails at the first import, as seen with the encodings error. Understanding CPython’s runtime expectations lets you design a deployable, deterministic package instead of a build-tree-dependent experiment. It also clarifies why distributing a single DLL is not realistic when you rely on CPython and C extensions.
Conclusion
The stable pattern for embedding Python in a C++ DLL on Windows is to ship the interpreter and its standard library with your application, point PYTHONHOME to that packaged folder, and initialize via Py_InitializeFromConfig. Keep the Python DLL where the loader can find it or explicitly load it. If you do not need CPython’s ecosystem, alternative interpreters can simplify distribution at the cost of extension compatibility. Plan for the security implications of shipping readable and modifiable Python code, and choose the trade-offs that fit your use case.