Python Packaging & Executables on Windows

I recently began developing on, and for Windows, and while I continue to develop my skills in Powershell and C#, I rely on my go-to language for most day-to-day automation, Python. I found that there were many marked differences between the experience of writing system programs for Unix-like systems, and for Windows, most of which I won’t go into in this post, but to highlight one that recently resulted in a little bit of poking around, writing Python packages and installing executable scripts via pip on Windows required becoming a bit more familiar with this part of the Python ecosystem.

Your typical Python package has a structure like:

Project
-> setup.py
-> package_name/__init__.py
-> package_name/package.py
-> bin/package_name

where, once your module is build, the script in bin imports it, and is, itself, installed to your PATH per your setup.py which might look like this:

from setuptools import setupsetup(name='package_name',version='0.1',...packages=['package_name'],scripts=['bin/package_name'],install_requires=[...],zip_safe=False)

and on Unix-like systems, this would be all that is required, you might not even bother with an entry_points dictionary for less complex packages with a handful of single functions, and most other routing for functions might happen from a CLI tool package like argparse .

However, when you build a package like this on Windows, you can, of course, execute the installed script, referencing the PATH to that script, but it won’t be interpretted correctly as a Python script as-is:

PS C:\Users\you\src> package_name
Program 'package_name' failed to run: No application is associated with the specified file for this operationAt line:1 char:1
+ wslrun
+ ~~~~~~.
At line:1 char:1
+ ~~~~~~
+ CategoryInfo : ResourceUnavailable: (:) [], ApplicationFailedException
+ FullyQualifiedErrorId : NativeCommandFailed

or executing the path to the script directly, i.e.:

C:\Python38\Scripts\package_name

A correct, and commonly executable method of doing this without creating aliases, etc. is using the entry_points option I mentioned a moment ago, where you notate a command and map to a function. One limitation is that these entry point console scripts do not allow you to define arguments, however, you can handle them, all the same.

Let’s assume bin\package_name had something like:

import package_nameimport sys, getopt, ostry:  opts, args = getopt.getopt(sys.argv[1:],"hi:o:", ["path =",""])except getopt.GetoptError:  print("-p 'path'")  sys.exit(2)for opt, arg in opts:  if opt in ("-p", "--path"):  path= argprint(package_name.main(path))

in it, in order to handle arguments to route user input to some function in your package_name/main.py file that’s imported at the beginning of this script.

To handle this, both, more correctly for Windows, and in a more condensed fashion, this behavior can be moved to your main script, like so:

def main():    try:       opts, args = getopt.getopt(sys.argv[1:],"hi:o:", ["path =",""])    except getopt.GetoptError:       print("-p 'path'")       sys.exit(2)   for opt, arg in opts:       if opt in ("-p", "--path"):       path= arg   print(main(path))

with the following caveats:

  1. main() should not accept arguments, either, so you’ll need a package like argparse (I used optparse here, but anything passed from sys.argv[1:] — all positional arguments from the first on- can be read by your package when we update the command in setup.py in a moment)

In your setup.py to create this mapping, you’ll need to remove the line:

# scripts=['bin/package_name'],

or comment it out (as I did above), and add:

entry_points = {    "console_scripts": [         "command_name = package_name:main",         ]    },

So, when you proceed to build the package:

pip install -e .

You can then run a command like this in PowerShell to see where the executable was dropped:

PS C:\Users\you\src> Get-Command command_name.exe

CommandType Name Version Source
----------- ---- ------- ------
Application command_name.exe 0.0.0.0 C:\Users\you\AppData\Local\Programs\Python\Python38\Scripts\command_name.exe

where command_name is whatever you named the command in the setup.py file, and you can proceed to use your command by calling it:

PS C:\Users\you\src> command_name.exe --path whatever.txt

Additional Resources

Some excellent reading if you’re new to Python packaging in general:

python-packaging

Hitchhiker’s Guide to Python Packaging

Python Apps the Right Way

Written by

Systems Engineer

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store