If you've tried packaging a PyQt5 application on macOS with PyInstaller, there's a good chance you've hit at least one brick wall. Missing data files, mysteriously huge bundles, cryptic errors on Big Sur — these are all common stumbling blocks.
In this article, we'll walk through the most frequent PyInstaller problems on macOS and how to solve them. Whether you're dealing with files that can't be found at runtime, a 800MB app bundle for a simple script, or OpenGL framework errors after upgrading to Big Sur, you'll find practical solutions here.
File paths break after bundling
One of the first things people run into is that data files — CSVs, images, config files — that sat happily next to your script during development suddenly can't be found once the app is bundled.
This happens because PyInstaller changes how your application runs. When you bundle your app, especially as a one-file executable, the files get extracted to a temporary directory at runtime. The working directory is no longer the folder your script lives in, so typical approaches like using __file__ or os.getcwd() to locate nearby files won't point where you expect.
How PyInstaller handles runtime paths
PyInstaller sets a special attribute sys._MEIPASS when your app is running from a bundle. This points to the folder where your bundled data files are extracted. When running unbundled (during development), this attribute doesn't exist.
You can write a small helper function that works in both situations:
import sys
import os
def resource_path(relative_path):
"""Get the absolute path to a resource, works for development and PyInstaller bundles."""
if hasattr(sys, '_MEIPASS'):
# Running as a PyInstaller bundle
base_path = sys._MEIPASS
else:
# Running in normal development mode
base_path = os.path.abspath(".")
return os.path.join(base_path, relative_path)
Then, anywhere you need to load a file:
import csv
csv_file = resource_path("mydata.csv")
with open(csv_file, "r") as f:
reader = csv.reader(f)
for row in reader:
print(row)
This approach works identically whether you're running your script directly with Python or running the bundled application.
Making sure your data files are included
You also need to tell PyInstaller to actually include your data files. In your .spec file, add them to the datas list:
Bring Your PyQt/PySide Application to Market — Specialized launch support for scientific and engineering software built using Python & Qt.
a = Analysis(
['main.py'],
datas=[
('mydata.csv', '.'),
('other_file.csv', '.'),
],
# ... rest of your config
)
The format is (source_path, destination_folder). Using '.' places the file at the root of the bundle.
If you're running PyInstaller from the command line rather than using a spec file, you can use the --add-data flag:
pyinstaller --add-data "mydata.csv:." main.py
Note the colon : separator on macOS and Linux (Windows uses a semicolon ;).
An alternative: Qt Resource System
You might have seen recommendations to use the Qt Resource System (qrc files) to embed data directly into your application. This sidesteps the path problem entirely because the resources are compiled into your Python code — there are no external files to locate at runtime.
This is a solid approach, especially for things like icons, images, and other static assets. But it's not a requirement. The resource_path function above works perfectly well for CSV files and other data that you'd rather keep as separate files.
Reducing bloated bundle sizes
An 800MB app bundle for a straightforward PyQt5 application is frustrating, but it's a common experience. The root cause is how Python's import system works.
Python imports are dynamic. If your code imports PyQt5, then any submodule of PyQt5 — QtNetwork, QtWebSockets, QtWebEngine, and so on — could potentially be accessed at runtime. PyInstaller can't always tell which submodules you'll actually use, so it takes the safe approach and includes everything it finds.
The same thing happens with other large packages. If you have Jupyter, IPython, or NumPy installed in your environment, PyInstaller may pull them in through indirect dependencies.
Exclude modules you don't need
PyInstaller's --exclude-module flag lets you explicitly drop packages from the bundle:
pyinstaller --exclude-module QtWebEngine \
--exclude-module QtNetwork \
--exclude-module QtWebSockets \
--exclude-module IPython \
--exclude-module jupyter \
--exclude-module notebook \
main.py
Or in your .spec file:
a = Analysis(
['main.py'],
excludes=[
'QtWebEngine',
'QtNetwork',
'QtWebSockets',
'IPython',
'jupyter',
'notebook',
],
# ...
)
After excluding unnecessary modules, rebuild and check the size. You should see a significant reduction.
Use a clean virtual environment
The single most effective way to keep bundle sizes down is to package from a clean virtual environment that only contains the packages your application actually needs. If you're using Anaconda with a large environment that includes Jupyter, IPython, and dozens of scientific packages, PyInstaller will try to pull many of them in.
Create a fresh virtual environment and install only what your app requires:
python3 -m venv packaging_env
source packaging_env/bin/activate
pip install pyqt5 pyinstaller
# Install any other packages your app actually imports
pip install requests # for example
Then run PyInstaller from within this clean environment. You'll often see bundle sizes drop dramatically — from 800MB to under 200MB is common.
Check what's being included
If your bundle is still larger than expected, you can investigate. PyInstaller generates a build/ directory with a warn-*.txt file and other logs that show which modules were detected and included. Look through these for surprises.
You can also inspect the .app bundle directly. On macOS, a .app is just a folder:
# Right-click the .app in Finder and choose "Show Package Contents"
# Or from the terminal:
ls -la dist/MyApp.app/Contents/MacOS/
This lets you see exactly what's in there and identify the large items.
macOS Big Sur framework errors
After upgrading to macOS Big Sur (11.0), many users started seeing errors like this when running PyInstaller:
ERROR: Can not find path /System/Library/Frameworks/OpenGL.framework/Versions/A/OpenGL
This happens because Apple fundamentally changed how system libraries work in Big Sur. System frameworks and libraries were moved into a shared cache and are no longer present as individual files on disk. PyInstaller (and the tools it depends on) expected to find these files at their traditional paths.
Update PyInstaller
The most straightforward fix is to update PyInstaller to a version that understands Big Sur's changes:
pip install --upgrade pyinstaller
Versions from 4.1 onward include fixes for Big Sur compatibility. If you're stuck on an older version for some reason, upgrading should be your first step.
Avoid Anaconda for packaging
Several users have reported that Anaconda environments cause additional problems with PyInstaller on macOS, particularly on Big Sur. The Anaconda Python distribution includes its own copies of various libraries, and these can conflict with what PyInstaller expects to find on the system.
If you're hitting persistent issues, try using the standard Python installation from python.org instead:
# Install Python from python.org, then:
python3 -m venv myenv
source myenv/bin/activate
pip install pyqt5 pyinstaller
pyinstaller main.py
This combination — standard CPython from python.org, a clean virtual environment, and the latest PyInstaller — gives you the best chance of a smooth packaging experience on macOS.
Alternative packaging tools for macOS
PyInstaller is the most commonly used option, but it's not the only one. Two other tools are worth knowing about:
py2app is a macOS-specific tool that creates .app bundles. It uses a similar freezing approach to PyInstaller, so bundle sizes will be comparable, but some users find it handles certain macOS-specific edge cases better.
pip install py2app
cx_Freeze is a cross-platform option that works on macOS, Windows, and Linux. Again, it uses the same general approach of bundling Python and your dependencies together.
pip install cx_freeze
All three tools produce similar results in terms of bundle size because the fundamental approach is the same: they bundle a Python interpreter along with your code and its dependencies. The differences are mainly in configuration, compatibility with specific packages, and how they handle platform-specific details.
A complete packaging workflow for macOS
Putting it all together, here's a reliable workflow for packaging a PyQt5 app on macOS:
# Create a clean virtual environment
python3 -m venv build_env
source build_env/bin/activate
# Install only what you need
pip install pyqt5 pyinstaller
# Install your app's other dependencies
pip install -r requirements.txt
# Build the app bundle
pyinstaller --windowed \
--name "MyApp" \
--add-data "mydata.csv:." \
--exclude-module QtWebEngine \
--exclude-module QtWebEngineCore \
--exclude-module QtWebEngineWidgets \
main.py
The --windowed flag (or -w) tells PyInstaller to create a proper macOS .app bundle without a console window, which is what you want for a GUI application.
After the build completes, you'll find your app in the dist/ folder. Test it thoroughly — open it from Finder, try all the features that load external files, and verify everything works as expected.
If you need to distribute the app to other users, you can compress the .app bundle into a .zip or create a .dmg disk image. For the .dmg approach, tools like create-dmg can automate the process and give you a professional-looking installer.
Packaging Python applications on macOS takes some patience and troubleshooting, but with a clean environment, the right PyInstaller settings, and awareness of the common pitfalls, you can produce a working app bundle that your users can run without needing Python installed on their machines.
PyQt6 Crash Course by Martin Fitzpatrick — The important parts of PyQt6 in bite-size chunks