How to Create a GUI/CLI Standalone App in Python with Gooey and PyInstaller

Vojtěch Škvařil
Analytics Vidhya
Published in
3 min readJan 15, 2021
GUI version

Creating GUI in Python with versatile Gooey is simple and well-documented in many places:

, but having CLI and GUI switchable in one executable seamlessly is a different story. Gooey is not originally meant to be used like that, although the library has an undocumented feature that makes this possible (so it can be either fully documented, or deleted in further Gooey releases). This tutorial can be also useful to those interested in Python script building.

CLI calling of the same script

Having both of these together can be great for use cases where one executable file is desirable to be distributed to users with different interface preferences.

The first part lies in --ignore-gooey command. That allows switching between GUI and CLI interface.

if len(sys.argv) >= 2:
if not "--ignore-gooey" in sys.argv:
sys.argv.append("--ignore-gooey")

This snippet triggers the ignore command when the app is launched without arguments (even from the command line) otherwise CLI is activated. Then the PyInstaller library takes place. If you want the app to fully work in GUI/CLI mode then you have to override the commonly recommended (and default) configuration option in .spec file console=False. With this option on, you can see both GUI and CLI, but GUI is accompanied by an ugly empty command line window. With the snippet below, this window is hidden when the GUI kicks in.

exe = EXE(pyz,
a.scripts,
[],
exclude_binaries=True,
name=’CLI GUI Example App’,
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
console=True )

At first, we check if the script is ‘frozen’/packaged for cases we run the script before wrapping-up. Also if the -ignore-gooey command was assigned in the previous step then the user runs the app with arguments from the command line.

if not "--ignore-gooey" in sys.argv and getattr(sys, "frozen", False)

Then we import libraries with bindings to Windows COM. For some might be confusing that the libraries starting win- come from library pywin32.

import win32process, win32gui, win32con    
import os

Getting the process ID (PID) of the running script is simple in Python but really depends if you build the script as one file (--onefile argument in PyInstaller) or folder (--onedir) standalone executable. If you use parent process ID (PPID) for --onefir option then the Windows Explorer will be hidden and you won’t see a lot from basic Windows UI (been there, done that — restart fix it). More about PyInstaller arguments here.

pid = os.getpid() # PID, use if --onedir
pid = os.getppid() # PPID, use if --onefile

It seems that Windows doesn’t have a more direct way how to link the opened window to the process, but this loop below fixes it. Don't worry the process won't be killed, only hidden in the background.

open_windows_with_corresponding_pid = get_hwnds_for_pid(pid)
for hwnd in open_windows_with_corresponding_pid:
try:
win32gui.ShowWindow(hwnd, win32con.SW_HIDE)
except Exception:
pass

The rest of the code is similar to other Gooey tutorials. The script counts characters in input and outputs the counted characters, you can see the logic and structure below.

Onedir or Onefile?

I prefer --onedir PyInstaller distribution as the script starts much faster (eg 1–5 seconds instead of 20–60 seconds, depends on script size and complexity). For some cases (eg. small scripts without a small amount of 3rd party libraries) compact one folder is more convenient.

py -m PyInstaller --onefile --clean "C:\ProjectName\script.py"
py -m PyInstaller --onedir --clean "C:\ProjectName\script.py"

That’s it, successful script distribution!

  • Another convenient way to package your application is cx_freeze library, setup is well described here or here.
  • How to package it alternatively with Nuitka.
  • Briefcase (installable executable)

--

--