Running Python script with QProcess after freeze

How to bundle and run external Python scripts from a frozen PyQt6 application
Heads up! You've already completed this tutorial.

When you freeze a PyQt6 application with PyInstaller, your app gets bundled into a self-contained package. But what happens when your app needs to launch an external Python script or tool using QProcess? If you're not careful, the process will try to use whatever Python is installed on the user's system — or fail entirely if Python isn't installed at all.

This is a common issue. You've carefully packaged your application, but the QProcess call reaches outside your bundle and tries to use the system Python. Let's look at why this happens and how to solve it.

Why QProcess uses the system Python

When you call QProcess.start() with a command like "mitmproxy", Qt searches the system PATH to find that executable. After freezing, your bundled application lives in its own directory, but QProcess has no idea about that — it just asks the operating system to find and run mitmproxy the same way a terminal would.

If the user has Python 2.7 installed system-wide (and mitmproxy is installed there), that's the one that gets picked up. If the user has no Python at all, the command fails.

The fundamental problem is that QProcess launches a separate process outside your frozen bundle. It doesn't inherit the Python environment you used when building the app.

Approaches to solve this

There are a few strategies depending on what exactly you need to run.

Bundle the script into your application

If the script you're running is your own code, the best approach is usually to avoid launching it as a separate process entirely. Instead, you can run it within your application using Python's threading or multiprocessing modules, or use Qt's multithreading with QThreadPool.

For example, if meta_proxy.py contains a function you can call directly:

python
import threading
from script.addons import meta_proxy


def run_proxy():
    meta_proxy.start(port=8888)


thread = threading.Thread(target=run_proxy, daemon=True)
thread.start()

This avoids the QProcess problem completely because the code runs inside your already-frozen application. PyInstaller will bundle meta_proxy.py and its dependencies along with everything else.

PyQt6 Crash Course by Martin Fitzpatrick — The important parts of PyQt6 in bite-size chunks

See the course

Bundle the external tool as a separate frozen executable

If you genuinely need to run a separate process — for example, because the tool manages its own event loop or needs process isolation — you can freeze the external script as its own standalone executable and ship it alongside your main app.

With PyInstaller, you can create a second frozen executable for the script:

sh
pyinstaller --onefile script/addons/meta_proxy.py

This produces a standalone meta_proxy (or meta_proxy.exe on Windows) binary that doesn't need Python installed. You then include this binary in your application's distribution and launch it with QProcess using its known path.

Here's how you'd reference it from your frozen app:

python
import os
import sys
import shlex

from PyQt6 import QtCore


def get_resource_path(relative_path):
    """Get the absolute path to a bundled resource."""
    if getattr(sys, "frozen", False):
        # Running as a frozen executable
        base_path = os.path.dirname(sys.executable)
    else:
        # Running in a normal Python environment
        base_path = os.path.dirname(os.path.abspath(__file__))
    return os.path.join(base_path, relative_path)


# Build the path to the bundled executable
proxy_path = get_resource_path("meta_proxy")

p = QtCore.QProcess()
p.start(proxy_path, ["-p", "8888", "-k"])

The get_resource_path helper figures out where your application is running from. When frozen, sys.executable points to your main .exe, so os.path.dirname(sys.executable) gives you the folder it lives in. You place the meta_proxy executable in that same folder (or a subfolder), and now QProcess can find it without relying on the system PATH.

Use the bundled Python interpreter

PyInstaller bundles a Python interpreter inside your frozen application. In some cases, you can use this interpreter to run scripts directly. The embedded interpreter is available at a path relative to your executable.

On macOS and Linux, the Python library is embedded in the bundle, but there isn't a standalone python executable you can easily call. On Windows with --onedir mode, you may find the Python DLL and supporting files in the output directory, but calling them directly is fragile and not recommended.

A more reliable variant of this approach is to use sys.executable itself. When your application is frozen with PyInstaller, sys.executable points to your frozen app's executable. You can make your frozen app accept command-line arguments that tell it to run in a "worker" mode instead of showing the GUI:

python
import sys
import os

from PyQt6 import QtWidgets, QtCore


def run_proxy(port):
    """Run the proxy service (called in subprocess mode)."""
    # Import and start the proxy here
    print(f"Starting proxy on port {port}")
    # meta_proxy.start(port=port)


def main():
    if len(sys.argv) > 1 and sys.argv[1] == "--run-proxy":
        # We're being called as a subprocess — run the proxy
        port = int(sys.argv[2]) if len(sys.argv) > 2 else 8888
        run_proxy(port)
        sys.exit(0)

    # Normal GUI mode
    app = QtWidgets.QApplication(sys.argv)

    window = QtWidgets.QMainWindow()
    window.setWindowTitle("My Application")

    button = QtWidgets.QPushButton("Start Proxy")

    def start_proxy():
        p = QtCore.QProcess()
        # Launch ourselves with --run-proxy flag
        p.start(sys.executable, ["--run-proxy", "8888"])
        # Keep a reference so the process isn't garbage collected
        window.process = p

    button.clicked.connect(start_proxy)
    window.setCentralWidget(button)
    window.show()

    sys.exit(app.exec())


if __name__ == "__main__":
    main()

This pattern is quite elegant: your frozen .exe serves double duty. When launched normally, it shows the GUI. When launched with --run-proxy, it runs the proxy logic and exits. Since sys.executable always points to the frozen binary, QProcess launches another copy of your own application — no system Python needed.

Handling mitmproxy specifically

In the original question, the command being run is mitmproxy, which is a third-party tool. You have a few options here:

  1. Bundle mitmproxy as a frozen executable. If mitmproxy provides a way to freeze it (or you can freeze its entry point), bundle it alongside your app and reference it by path as shown above.

  2. Use mitmproxy as a library. The mitmproxy package exposes a Python API. Instead of launching it as a separate process, you can import and run it directly inside your application (in a thread). This way PyInstaller bundles all the mitmproxy dependencies automatically.

  3. Ship a known Python environment. You could bundle a portable Python installation with your app and point QProcess at that specific interpreter. This works but significantly increases your distribution size and complexity.

Option 2 is usually the cleanest. For example, you can use mitmproxy's programmatic API:

python
import threading
from mitmproxy.tools.main import mitmproxy


def start_mitmproxy():
    mitmproxy(args=["-p", "8888", "-s", "addons/meta_proxy.py", "-k"])


thread = threading.Thread(target=start_mitmproxy, daemon=True)
thread.start()

Because this runs inside your frozen application's process, PyInstaller handles all the dependencies.

Summary of approaches

Approach When to use it
Run the code directly in a thread When the script is your own code or a library with a Python API
Freeze the script as a second executable When you need true process isolation
Use sys.executable with a flag When you want one frozen binary that can act as both GUI and worker
Bundle a portable Python Last resort — adds size and complexity

The common thread across all these solutions is the same: after freezing, you can't rely on the user's system having Python or any Python packages installed. Everything your application needs must be included in what you ship. Whether that means importing a library directly, bundling a second executable, or re-launching your own frozen app in a different mode, the goal is to keep everything self-contained.

For more details on using QProcess to run external programs, see our complete guide to QProcess in PyQt6. If you're looking for a step-by-step walkthrough of packaging your PyQt6 app with PyInstaller on Windows, check out our PyQt6 PyInstaller packaging tutorial.

Create GUI Applications with Python & Qt6 by Martin Fitzpatrick — (PyQt6 Edition) The hands-on guide to making apps with Python — Save time and build better with this book. Over 15K copies sold.

Get the book

Well done, you've finished this tutorial! Mark As Complete
[[ user.completed.length ]] completed [[ user.streak+1 ]] day streak
Martin Fitzpatrick

Running Python script with QProcess after freeze was written by Martin Fitzpatrick.

Martin Fitzpatrick has been developing Python/Qt apps for 8 years. Building desktop applications to make data-analysis tools more user-friendly, Python was the obvious choice. Starting with Tk, later moving to wxWidgets and finally adopting PyQt. Martin founded PythonGUIs to provide easy to follow GUI programming tutorials to the Python community. He has written a number of popular Python books on the subject.