This is a cautionary tale about how embedded Python finds its runtime files under Windows. Don’t worry, though — everyone lives happily ever after.
The story begins with a client’s request to build an executable under Windows that embeds Python. This is not so hard; there is documentation for embedding Python.
I had two versions of Python installed on my Windows virtual machine because I was experimenting with different Pythons at the time. (Doesn’t everyone go through an experimental phase in their youth?) In
C:\Python27 I had installed 64-bit Python 2.7.9 from Python.org, and in
C:\Users\philip\Miniconda2 I had installed 64-bit Python 2.7.11 from Continuum. The 2.7.9 version was an older install. I was only interested in using the 2.7.11 version.
My executable’s C code told Python where to find its runtime files —
After compiling and linking, I ran my Python-embedding executable which imported my
hello_world.py file. It printed “Hello world!” as expected.
Here Come the Dragons
I thought everything was fine until I added these print statements to my Python code —
import sys print sys.exec_prefix print sys.version
The output was not what I expected —
C:/Users/philip/Miniconda2 2.7.9 (default, Dec 10 2014, 12:28:03) [MSC v.1500 64 bit (AMD64)]
This is contradictory! The Miniconda Python was 2.7.11, yet
sys.version identified itself as Python 2.7.9. How can the same module report two different Pythons simultaneously?
What Went Wrong
sys module reported information about two Pythons simultaneously because I was mixing parts from two different Python runtimes simultaneously.
Python’s runtime consists of a dynamically loaded library (
python27.dll), the standard library which is largely written in Python itself, and an executable. The executable is usually
python.exe, but in this case it was my C program that embedded Python. When my C program asked Windows to find
python27.dll, Windows searched for it in these directories as documented in the Windows DLL search strategy —
- The directory where the executable module for the current process is located.
- The current directory.
- The Windows system directory (usually
- The Windows directory (usually
- The directories listed in the PATH environment variable.
My problem was that Windows found
C:\Windows\system32\python27.dll first, and that was from my Python 2.7.9 installation. Meanwhile, my call to
Py_SetPythonHome() had told Python to use the standard library from Miniconda Python. The value of
sys.version comes from a string hardcoded in the runtime DLL, while
sys.exec_prefix is derived from the value I passed in
Py_SetPythonHome(). I was using the standard library from one Python installation, and the runtime DLL from another.
Although I didn’t experiment with this for long, I might not have noticed that there was a problem if I hadn’t been lucky enough to double check my Python setup with the
sys module. The standard library probably doesn’t care about which interpreter it runs under. I can imagine a few cases may exist where changes/bug fixes were made to the Python part of the standard library for versions 2.7.10 and 2.7.11 that rely on corresponding changes to the binary runtime, and that code might behave badly.
Both of the Pythons I was using were built with the same compiler, so theoretically binary extensions like
numpy should run just fine under either Python. But I could certainly forgive
numpy if it crashed as a result.
In short, this is neither a typical nor a supported use of Python which puts it in the “Here there be dragons” category.
The solution was very simple. I copied
C:\Users\philip\Miniconda2\python27.dll into the same directory as my custom executable. Since that’s the first location Windows searches when loading a DLL, it isolates my code from other Python DLLs that might appear in (or disappear from) other locations in the file system. Problem solved!