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
.uifile 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:
python3 -m venv myapp_env
source myapp_env/bin/activate
Now install the packages your application needs:
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:
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:
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:
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:
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_FILESmust include your.uifile. 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": Falseavoids 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:
python setup.py py2app -A
Open the generated .app file from the dist/ folder:
open dist/main.app
If that works, build the full standalone version:
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:
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:
pyuic6 mainwindow.ui -o ui_mainwindow.py
Then import the generated module directly:
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:
./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:
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:
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:
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.
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.