|
| 1 | +.. _`Windows applications`: |
| 2 | + |
| 3 | +================================================= |
| 4 | +Creating a Windows application that embeds Python |
| 5 | +================================================= |
| 6 | + |
| 7 | + |
| 8 | +Overview |
| 9 | +======== |
| 10 | + |
| 11 | + |
| 12 | +Why embed Python? |
| 13 | +----------------- |
| 14 | + |
| 15 | +When writing an application on Windows, whether command line or GUI, it |
| 16 | +integrates much better with the operating system if the application is delivered |
| 17 | +as a native Windows executable. However, Python is not a natively compiled |
| 18 | +language, and so does not create executables by default. |
| 19 | + |
| 20 | +The normal way around this issue is to make your Python code into a library, and |
| 21 | +declare one or more "script entry points" for the library. When the library is |
| 22 | +installed, the installer will generate a native executable which invokes the |
| 23 | +Python interpreter, calling your entry point function. This is a very effective |
| 24 | +solution, and is used by many Python applications. It is supported by utilities |
| 25 | +such as ``pipx`` and ``uv tool``, which make managing such entry points (and the |
| 26 | +virtual environments needed to support them) easy. |
| 27 | + |
| 28 | +There are, however, some downsides to this approach. The entry point wrapper |
| 29 | +results in a chain of processes being created - the wrapper itself, the virtual |
| 30 | +environment redirector, and finally the Python interpreter. Creating all these |
| 31 | +processes isn't cheap, and particularly for a small command line utility, it |
| 32 | +impacts application startup time noticeably. Furthermore, the entry point |
| 33 | +wrapper is a standard executable with a zipfile attached - because of this, the |
| 34 | +application cannot be signed in advance by the developer, and this is often seen |
| 35 | +as "suspicious" by virus scanners. If a scan is triggered, this can make the |
| 36 | +application even slower to start, as well as running the risk of "false |
| 37 | +positives", where an innocent app is flagged as malicious. |
| 38 | + |
| 39 | +In addition, you may not want to expose your users to the fact that you wrote |
| 40 | +your code in Python. The implementation language should not be something your |
| 41 | +users need to care about. |
| 42 | + |
| 43 | +If any of these issues matter to you, you should consider writing your |
| 44 | +application in Python, but then embedding it, and the Python interpreter, into a |
| 45 | +native executable application that you can ship to your users. This does require |
| 46 | +you to write a small amount of native code (typically in C), but the code is |
| 47 | +mostly boilerplate and easy to maintain. |
| 48 | + |
| 49 | + |
| 50 | +What will your application look like? |
| 51 | +------------------------------------- |
| 52 | + |
| 53 | +When embedding Python, it is not possible to create a "single file" application. |
| 54 | +The Python interpreter itself is made up of multiple files, so you will need |
| 55 | +those at a minimum. However, once you have decided to ship your application as |
| 56 | +multiple files, it becomes very easy to structure your code. |
| 57 | + |
| 58 | +There are basically three "parts" to an embedded application: |
| 59 | + |
| 60 | +1. The main executable that the user runs. |
| 61 | +2. The Python interpreter. |
| 62 | +3. Your application code, written in Python. |
| 63 | + |
| 64 | +You can, if you wish, dump all of those items into a single directory. However, |
| 65 | +it is much easier to manage the application if you keep them separate. |
| 66 | +Therefore, the recommended layout is:: |
| 67 | + |
| 68 | + Application directory |
| 69 | + MyAwesomePythonApp.exe |
| 70 | + interp |
| 71 | + (embedded Python interpreter) |
| 72 | + lib |
| 73 | + (Python code implementing the application) |
| 74 | + |
| 75 | +The remainder of this guide will assume this layout. |
| 76 | + |
| 77 | + |
| 78 | +How to build your application |
| 79 | +============================= |
| 80 | + |
| 81 | +Writing the Python code |
| 82 | +----------------------- |
| 83 | + |
| 84 | +Your Python application should be runnable by invoking a single function in your |
| 85 | +application code. Typically, this function will be called ``main`` and will be |
| 86 | +located in the root package of your application, but that isn't a hard |
| 87 | +requirement. If you prefer to locate the function somewhere else, all you will |
| 88 | +need to do is make a small modification to the wrapper code. |
| 89 | + |
| 90 | +Your code can use 3rd party dependencies freely. These will be installed along |
| 91 | +with your application. |
| 92 | + |
| 93 | +When you are ready to build your application, you can install the Python code |
| 94 | +using:: |
| 95 | + |
| 96 | + pip install --target "<Application directory>\lib" MyAwesomePythonApp |
| 97 | + |
| 98 | +You can then run your application as follows:: |
| 99 | + |
| 100 | + $env:PYTHONPATH="<Application directory>\lib" |
| 101 | + python -c "from MyAwesomePythonApp import main; main()" |
| 102 | + |
| 103 | +Note that this uses your system Python interpreter. This will not be the case |
| 104 | +for the final app, but it is useful to test that the Python code has been |
| 105 | +installed correctly. |
| 106 | + |
| 107 | +If that works, congratulations! You have successfully created the Python part of |
| 108 | +your application. |
| 109 | + |
| 110 | +The embedded interpreter |
| 111 | +------------------------ |
| 112 | + |
| 113 | +You can download embeddable builds of Python from |
| 114 | +https://www.python.org/downloads/windows/. You want the "Windows embeddable |
| 115 | +package". There are usually 3 versions, for 64-bit, 32-bit and ARM64 |
| 116 | +architectures. Generally, you should use the 64-bit version unless you have a |
| 117 | +specific need for one of the others (in which case, you will need to modify how |
| 118 | +you compile the main application executable slightly, to match). |
| 119 | + |
| 120 | +Simply unpack the downloaded zip file into the "interp" subdirectory of your |
| 121 | +application layout. |
| 122 | + |
| 123 | +In order for your embedded interpreter to be able to find your application code, |
| 124 | +you should modify the `python*._pth` directory contained in the distribution. By |
| 125 | +default it looks like this:: |
| 126 | + |
| 127 | + python313.zip |
| 128 | + . |
| 129 | + |
| 130 | + # Uncomment to run site.main() automatically |
| 131 | + #import site |
| 132 | + |
| 133 | +You need to add a single line, ``../lib``, after the line with the dot. The |
| 134 | +resulting file will look like this:: |
| 135 | + |
| 136 | + python313.zip |
| 137 | + . |
| 138 | + ../lib |
| 139 | + |
| 140 | + # Uncomment to run site.main() automatically |
| 141 | + #import site |
| 142 | + |
| 143 | +If you have put your application Python code somewhere else, this is the only |
| 144 | +thing you need to change. The file contains a list of directories (relative to |
| 145 | +the interpreter directory) which will be added to Python's ``sys.path`` when |
| 146 | +starting the interpreter. |
| 147 | + |
| 148 | +The driver application |
| 149 | +---------------------- |
| 150 | + |
| 151 | +This is the only part of your application that has to be written in C. The |
| 152 | +application code should look like the following:: |
| 153 | + |
| 154 | + /* Include the Python headers */ |
| 155 | + #include <Python.h> |
| 156 | + |
| 157 | + /* Finding the Python interpreter */ |
| 158 | + #include <windows.h> |
| 159 | + #include <pathcch.h> |
| 160 | + |
| 161 | + /* Tell the Visual Studio linker what libraries we need */ |
| 162 | + #pragma comment(lib, "delayimp") |
| 163 | + #pragma comment(lib, "pathcch") |
| 164 | + |
| 165 | + int dll_dir(wchar_t *path) { |
| 166 | + wchar_t interp_dir[PATHCCH_MAX_CCH]; |
| 167 | + if (GetModuleFileNameW(NULL, interp_dir, PATHCCH_MAX_CCH) && |
| 168 | + SUCCEEDED(PathCchRemoveFileSpec(interp_dir, PATHCCH_MAX_CCH)) && |
| 169 | + SUCCEEDED(PathCchCombineEx(interp_dir, PATHCCH_MAX_CCH, interp_dir, path, PATHCCH_ALLOW_LONG_PATHS)) && |
| 170 | + SetDefaultDllDirectories(LOAD_LIBRARY_SEARCH_DEFAULT_DIRS) && |
| 171 | + AddDllDirectory(interp_dir) != 0) { |
| 172 | + return 1; |
| 173 | + } |
| 174 | + return 0; |
| 175 | + } |
| 176 | + |
| 177 | + /* Your application main program */ |
| 178 | + int wmain(int argc, wchar_t **argv) |
| 179 | + { |
| 180 | + PyStatus status; |
| 181 | + PyConfig config; |
| 182 | + |
| 183 | + /* Tell the loader where to find the Python interpreter. |
| 184 | + * This is the name, relative to the directory containing |
| 185 | + * the application executable, of the directory where you |
| 186 | + * placed the embeddable Python distribution. |
| 187 | + * |
| 188 | + * This MUST be called before any functions from the Python |
| 189 | + * runtime are called. |
| 190 | + */ |
| 191 | + if (!dll_dir(L"interp")) |
| 192 | + return -1; |
| 193 | + |
| 194 | + /* Initialise the Python configuration */ |
| 195 | + PyConfig_InitIsolatedConfig(&config); |
| 196 | + /* Pass the C argv array to ``sys.argv`` */ |
| 197 | + PyConfig_SetArgv(&config, argc, argv); |
| 198 | + /* Install the standard Python KeyboardInterrupt handler */ |
| 199 | + config.install_signal_handlers = 1; |
| 200 | + /* Initialise the runtime */ |
| 201 | + status = Py_InitializeFromConfig(&config); |
| 202 | + /* Deal with any errors */ |
| 203 | + if (PyStatus_Exception(status)) { |
| 204 | + PyConfig_Clear(&config); |
| 205 | + if (PyStatus_IsExit(status)) { |
| 206 | + return status.exitcode; |
| 207 | + } |
| 208 | + Py_ExitStatusException(status); |
| 209 | + return -1; |
| 210 | + } |
| 211 | + |
| 212 | + /* CPython is now initialised. |
| 213 | + * Now load and run your application code. |
| 214 | + */ |
| 215 | + |
| 216 | + int exitCode = -1; |
| 217 | + PyObject *module = PyImport_ImportModule("MyAwesomePythonApp"); |
| 218 | + if (module) { |
| 219 | + // Pass any more arguments here |
| 220 | + PyObject *result = PyObject_CallMethod(module, "main", NULL); |
| 221 | + if (result) { |
| 222 | + exitCode = 0; |
| 223 | + Py_DECREF(result); |
| 224 | + } |
| 225 | + Py_DECREF(module); |
| 226 | + } |
| 227 | + if (exitCode != 0) { |
| 228 | + PyErr_Print(); |
| 229 | + } |
| 230 | + Py_Finalize(); |
| 231 | + return exitCode; |
| 232 | + } |
| 233 | + |
| 234 | + |
| 235 | +Almost all of this is boilerplate that you can copy unchanged into your |
| 236 | +application, if you wish. |
| 237 | + |
| 238 | +You should change the name of the module that gets imported, and if you chose a |
| 239 | +different name for your main function, you should change that as well. |
| 240 | +Everything else can be left unaltered. |
| 241 | + |
| 242 | +If you want to customise the way the interpreter is run, or set up the |
| 243 | +environment in a specific way, you can do so by modifying this code. However, |
| 244 | +such modifications are out of scope for this guide. If you want to make such |
| 245 | +changes, you should be familiar with the relevant parts of the Python C API |
| 246 | +documentation and the Windows API. |
| 247 | + |
| 248 | +Building the driver application |
| 249 | +------------------------------- |
| 250 | + |
| 251 | +To build the driver application, you will need a copy of Visual Studio, and a |
| 252 | +full installation of the same version of Python as you are using for the |
| 253 | +embedded interpreter. The reason for the full Python installation is that the |
| 254 | +embedded version does not include the necessary C headers and library files to |
| 255 | +build code using the Python C API. |
| 256 | + |
| 257 | +It may be possible to use a C compiler other than Visual Studio, but if you wish |
| 258 | +to do this, you will need to work out how to do the build, including the |
| 259 | +necessary delay loading, yourself. |
| 260 | + |
| 261 | +To compile the code, you need to know the location of the Python headers and |
| 262 | +library files. You can get these locations from the interpreter as follows:: |
| 263 | + |
| 264 | + import sysconfig |
| 265 | + |
| 266 | + print("Include files:", sysconfig.get_path("include")) |
| 267 | + print("Library files:", sysconfig.get_config_var("LIBDIR")) |
| 268 | + |
| 269 | +To build your application, you can then simply use the following commands:: |
| 270 | + |
| 271 | + cl /c /Fo:main.obj main.c /I<Include File Location> |
| 272 | + link main.obj /OUT:MyAwesomePythonApp.exe /DELAYLOAD:python313.dll /LIBPATH:<Lib File Location> |
| 273 | + |
| 274 | +You should use the correct Python version in the ``/DELAYLOAD`` argument, based |
| 275 | +on the name of the DLL in your embedded distribution. For a production build, |
| 276 | +you might want additional options, such as optimisation (although the wrapper |
| 277 | +exe is small enough that optimisation might not make a significant difference). |
| 278 | + |
| 279 | +If you place the resulting `exe` file in your application target directory, and |
| 280 | +run it, your application should run, exactly the same as it did when you invoked |
| 281 | +it using Python directly. |
| 282 | + |
| 283 | +Why do we delay load Python? |
| 284 | +---------------------------- |
| 285 | + |
| 286 | +In order to run the application, it needs to be able to find the Python |
| 287 | +interpreter. This is handled by the linker, as with any other referenced DLL. |
| 288 | +However, by default your embedded Python interpreter will not be on the standard |
| 289 | +search path for DLLs, and as a result your application will fail, or will pick |
| 290 | +up the wrong Python installation. By delay loading Python, we allow our code to |
| 291 | +change the search path *before* loading the interpreter. This is handled by the |
| 292 | +``dll_dir`` function in the application code. |
| 293 | + |
| 294 | +It *is* possible to create an application without using delay loading, but this |
| 295 | +requires that the Python distribution is unpacked in the root of your |
| 296 | +application directory. The recommended approach achieves a cleaner separation of |
| 297 | +the various parts of the application. |
| 298 | + |
| 299 | + |
| 300 | +Taking things further |
| 301 | +===================== |
| 302 | + |
| 303 | +Distributing your application |
| 304 | +----------------------------- |
| 305 | + |
| 306 | +Now that you have your application, you will want to distribute it. There are |
| 307 | +many ways of doing this, from simply publishing a zip of the application |
| 308 | +directory and asking your users to unpack it somewhere appropriate, to |
| 309 | +full-scale installers. This guide doesn't cover installers, as they are a |
| 310 | +complex subject of their own. However, the requirements of a Python application |
| 311 | +built this way are fairly trivial (unpack the application directory and provide |
| 312 | +a way for the user to run the exe), so most of the complexity is unneeded (but |
| 313 | +it's there if you have special requirements). |
| 314 | + |
| 315 | +Sharing code |
| 316 | +------------ |
| 317 | + |
| 318 | +Until now, we've assumed that you have one application, with its own Python code |
| 319 | +and its own interpreter. This is the simplest case, but you may have a suite of |
| 320 | +applications, and not want to have the overhead of an interpreter for each. Or |
| 321 | +you may have a lot of common Python code, with many different entry points. |
| 322 | + |
| 323 | +This is fine - it's easy to modify the layout to cover these cases. You can have |
| 324 | +as many executable files in the application directory as you want. These can |
| 325 | +all call their own entry point - they can even use completely independent |
| 326 | +libraries of Python code, although in that case you'd need to add some code to |
| 327 | +manipulate ``sys.path``. |
| 328 | + |
| 329 | +The point is that the basic structure can be as flexible as you want it to be - |
| 330 | +but it's better to start simple and add features as you need them, so that you |
| 331 | +don't have to maintain code that handles cases you don't care about. |
| 332 | + |
| 333 | + |
| 334 | +Potential Issues |
| 335 | +================ |
| 336 | + |
| 337 | +Using tkinter |
| 338 | +------------- |
| 339 | + |
| 340 | +The embedded Python distribution does not include tkinter. If your application |
| 341 | +needs a GUI, the simplest option is likely to be to use one of the other GUI |
| 342 | +frameworks available from PyPI, such as PyQt or wxPython. |
| 343 | + |
| 344 | +If your only option is tkinter, you will need to add a copy to the embedded |
| 345 | +distribution, or use a different distribution. Both of these options are outside |
| 346 | +the scope of this guide, however. |
| 347 | + |
| 348 | +Subprocesses and ``sys.executable`` |
| 349 | +----------------------------------- |
| 350 | + |
| 351 | +A common pattern in Python code is to run a Python subprocess using |
| 352 | +``subprocess.run([sys.executable, ...])``. This will not work for an embedded |
| 353 | +application, as ``sys.executable`` is your application, not the Python |
| 354 | +interpreter. |
| 355 | + |
| 356 | +The embedded distribution does contain a Python interpreter, which can be used |
| 357 | +in cases like this, but you will need to locate it yourself:: |
| 358 | + |
| 359 | + python_executable = Path(sys.executable).parent / ("interp/python.exe") |
| 360 | + |
| 361 | +If you are using the ``multiprocessing`` module, it has a specific method you |
| 362 | +must use to configure it to work correctly in an embedded environment, |
| 363 | +documented `in the Library reference |
| 364 | +<https://docs.python.org/3.13/library/multiprocessing.html#multiprocessing.set_executable>`_. |
| 365 | + |
| 366 | + |
| 367 | +What about other operating systems? |
| 368 | +=================================== |
| 369 | + |
| 370 | +This guide only applies to Windows. On other operating systems, there is no |
| 371 | +"embeddable" build of Python (at least, not at the time of writing). On the |
| 372 | +positive side, though, operating systems other than Windows have less need for |
| 373 | +this, as support for interpreted code as applications is generally better. In |
| 374 | +particular, on Unix a Python file with a "shebang" line is treated as a |
| 375 | +first-class application, and there is no benefit to making a native |
| 376 | +appliocation. |
| 377 | + |
| 378 | +So while this discussion is specific to Windows, the problem it is solving is |
| 379 | +*also* unique to Windows. |
0 commit comments