Skip to content

Commit 59487bf

Browse files
committed
Add a guide on how to create native Windows apps
1 parent fe434c3 commit 59487bf

File tree

2 files changed

+380
-0
lines changed

2 files changed

+380
-0
lines changed

source/guides/section-build-and-publish.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,4 @@ Building and Publishing
1616
making-a-pypi-friendly-readme
1717
publishing-package-distribution-releases-using-github-actions-ci-cd-workflows
1818
modernize-setup-py-project
19+
windows-applications-embedding
Lines changed: 379 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,379 @@
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

Comments
 (0)