Deploying PyQt6 Apps on macOS with py2app

How to package your PyQt6 application into a standalone macOS .app bundle
Heads up! You've already completed this tutorial.

I've got my PyQt app working, but when I try to build it into a macOS .app bundle using py2app, the resulting application crashes on launch with "quit unexpectedly." I've stripped my code down to the bare minimum — it just loads a .ui file and displays a window — but the built app still won't run. What's the easiest way to deploy a PyQt application on a Mac?

If you've ever spent days wrestling with packaging tools only to get a crash on launch, you're not alone. Getting a PyQt app running in your editor is one thing; turning it into a distributable macOS application is a different challenge entirely. The good news is that py2app works well once you understand a few common stumbling blocks — especially around Python environments and .ui files.

This guide walks you through deploying a PyQt6 application on macOS using py2app, step by step.

Why Does py2app Crash? The Anaconda Problem

One of the most common causes of mysterious py2app crashes is building from an Anaconda environment. Anaconda bundles its own copies of many libraries and has a complex environment structure that py2app doesn't always handle correctly. The result is an app that builds without errors but crashes immediately when you try to open it.

The fix is straightforward: use a standard Python virtual environment instead of Anaconda when building your app.

Setting Up a Clean Virtual Environment

Start by creating a fresh virtual environment using Python's built-in venv module. Open a terminal and run:

bash
python3 -m venv myapp_env
source myapp_env/bin/activate

Now install the packages your application needs:

bash
pip install PyQt6 py2app

This gives you a clean, minimal Python environment — exactly what py2app needs to correctly identify and bundle your dependencies.

Preparing Your Application

Let's work with a simple example application. Here's a minimal PyQt6 app that loads a .ui file created in Qt Designer:

python
import sys
from PyQt6 import QtWidgets as qtw
from PyQt6 import uic


Ui_MainForm, baseClass = uic.loadUiType("mainwindow.ui")


class MainWindow(baseClass):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        self.ui = Ui_MainForm()
        self.ui.setupUi(self)

        self.show()


if __name__ == "__main__":
    app = qtw.QApplication(sys.argv)
    w = MainWindow()
    sys.exit(app.exec())

Make sure this runs correctly from the command line before you try to package it:

bash
python main.py

If the window shows up, you're ready to package.

Creating the setup.py File

py2app uses a setup.py file to know how to build your application. You can generate a starter one automatically:

bash
py2applet --make-setup main.py

This creates a basic setup.py. However, you'll need to edit it to include your .ui file. Open setup.py and make sure it looks something like this:

python
from setuptools import setup

APP = ["main.py"]
DATA_FILES = ["mainwindow.ui"]
OPTIONS = {
    "argv_emulation": False,
    "packages": ["PyQt6"],
}

setup(
    app=APP,
    data_files=DATA_FILES,
    options={"py2app": OPTIONS},
    setup_requires=["py2app"],
)

There are a few things to note here:

  • DATA_FILES must include your .ui file. If py2app doesn't bundle it, your app will crash at runtime because it can't find the file.
  • "packages": ["PyQt6"] tells py2app to include the entire PyQt6 package. Without this, py2app sometimes misses Qt plugins or submodules, leading to crashes.
  • "argv_emulation": False avoids a known issue on newer versions of macOS where argv emulation can cause launch failures.

Building the Application

First, test with alias mode. This creates an app bundle that links back to your source files rather than copying them — useful for quick testing:

bash
python setup.py py2app -A

Open the generated .app file from the dist/ folder:

bash
open dist/main.app

If that works, build the full standalone version:

bash
python setup.py py2app

This takes longer because py2app is copying Python, PyQt6, Qt libraries, and all dependencies into the app bundle. When it finishes, you'll find the distributable .app in the dist/ folder.

Handling .ui Files at Runtime

There's a subtlety with .ui files that catches a lot of people out. When your app runs from a .app bundle, the working directory isn't what you might expect. The .ui file is bundled inside the app, but uic.loadUiType("mainwindow.ui") looks for it relative to the current working directory, which may not be where the file actually is.

To make your .ui file loading work both during development and inside a packaged app, use a path relative to the script's location:

python
import os
import sys
from PyQt6 import QtWidgets as qtw
from PyQt6 import uic

# Get the directory where this script (or frozen app) lives
if getattr(sys, "frozen", False):
    # Running inside a py2app bundle
    basedir = os.path.dirname(sys.executable)
    # Data files are in ../Resources relative to the executable
    basedir = os.path.join(basedir, "..", "Resources")
else:
    basedir = os.path.dirname(os.path.abspath(__file__))

ui_path = os.path.join(basedir, "mainwindow.ui")
Ui_MainForm, baseClass = uic.loadUiType(ui_path)


class MainWindow(baseClass):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        self.ui = Ui_MainForm()
        self.ui.setupUi(self)

        self.show()


if __name__ == "__main__":
    app = qtw.QApplication(sys.argv)
    w = MainWindow()
    sys.exit(app.exec())

When py2app bundles your application, data files listed in DATA_FILES end up in the Contents/Resources directory inside the .app bundle. The code above accounts for this by checking sys.frozen — an attribute that py2app sets when your code is running inside a bundle.

An Alternative: Skip .ui Files Entirely

Another approach that avoids file-path issues altogether is to convert your .ui file to Python code before packaging. You can do this with the pyuic6 tool:

bash
pyuic6 mainwindow.ui -o ui_mainwindow.py

Then import the generated module directly:

python
import sys
from PyQt6 import QtWidgets as qtw
from ui_mainwindow import Ui_MainWindow


class MainWindow(qtw.QMainWindow, Ui_MainWindow):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.setupUi(self)
        self.show()


if __name__ == "__main__":
    app = qtw.QApplication(sys.argv)
    w = MainWindow()
    sys.exit(app.exec())

With this approach, py2app treats the UI as regular Python code — no data files to worry about, no path issues. This is generally the most reliable approach for packaging. For more on designing interfaces with Qt Designer, see our tutorial on using Qt Designer with PyQt6.

Troubleshooting Checklist

If your built app crashes on launch, work through this list:

Use a clean virtual environment. Don't build from Anaconda or conda. Create a fresh venv, install only what you need, and build from there.

Include all data files. Any file your application loads at runtime — .ui files, images, config files — must be listed in DATA_FILES in your setup.py.

Include the full PyQt6 package. Add "packages": ["PyQt6"] to your py2app options to avoid missing submodules or Qt plugins.

Disable argv emulation. Set "argv_emulation": False in your options. This feature is known to cause problems on recent macOS versions.

Check the crash log. When an app crashes, macOS generates a crash report. Look for Python tracebacks or missing library errors — they often point directly at the problem.

Run from the terminal for output. Instead of double-clicking the .app, run the executable directly to see error messages:

bash
./dist/main.app/Contents/MacOS/main

This prints any Python exceptions to the terminal, which is far more useful than a macOS crash dialog.

If you'd like to explore other packaging approaches, you may also want to look at packaging PyQt6 applications with PyInstaller on macOS, which can create .dmg installers.

Complete Working Example

Here's everything together. This example converts the .ui file to Python to avoid path issues, which is the most reliable approach for packaging.

setup.py:

python
from setuptools import setup

APP = ["main.py"]
DATA_FILES = []
OPTIONS = {
    "argv_emulation": False,
    "packages": ["PyQt6"],
}

setup(
    app=APP,
    data_files=DATA_FILES,
    options={"py2app": OPTIONS},
    setup_requires=["py2app"],
)

main.py:

python
import sys
from PyQt6 import QtWidgets as qtw
from ui_mainwindow import Ui_MainWindow


class MainWindow(qtw.QMainWindow, Ui_MainWindow):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.setupUi(self)
        self.show()


if __name__ == "__main__":
    app = qtw.QApplication(sys.argv)
    w = MainWindow()
    sys.exit(app.exec())

Build commands:

bash
python3 -m venv build_env
source build_env/bin/activate
pip install PyQt6 py2app
pyuic6 mainwindow.ui -o ui_mainwindow.py
python setup.py py2app

Your finished .app bundle will be in the dist/ folder, ready to share. Deploying PyQt6 apps on macOS does take a bit of setup, but once you have a working recipe like this, you can reuse it for all your projects. If you're new to PyQt6, our guide to creating your first window is a great place to start before diving into packaging.

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

Packaging Python Applications with PyInstaller by Martin Fitzpatrick

This step-by-step guide walks you through packaging your own Python applications from simple examples to complete installers and signed executables.

More info Get the book

Martin Fitzpatrick

Deploying PyQt6 Apps on macOS with py2app 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.