I want to release a PyQt app for macOS that sits in the menubar and needs to launch on login. I've found tutorials for doing this in Swift/Objective-C, but how do I achieve this with a Python/PyQt app?
If you're building a menubar utility or background tool with PyQt, you'll probably want it to start automatically when the user logs in. On macOS, the way to do this is with Launch Agents — small configuration files that tell the system to run a program at login time.
The good news is that this doesn't require Swift or Objective-C. You can set it up entirely with a simple .plist file, and even automate it from within your PyQt app so users can toggle "Run at startup" from your interface.
How macOS handles startup applications
macOS has two mechanisms for running things automatically:
- Launch Daemons — start at boot, before any user logs in. These live in
/Library/LaunchDaemonsor/System/Library/LaunchDaemonsand require root privileges. Not what you want for a user-facing app. - Launch Agents — start at login, in the context of a specific user. These live in
~/Library/LaunchAgents. This is the right choice for a PyQt application.
A Launch Agent is defined by a .plist (property list) file placed in the user's ~/Library/LaunchAgents/ folder. When the user logs in, macOS reads every .plist in that folder and launches whatever they describe.
Creating a Launch Agent .plist file
Here's an example .plist file that tells macOS to launch a PyQt app called "MyApp" from the /Applications folder at login:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.mycompany.myapp</string>
<key>LimitLoadToSessionType</key>
<string>Aqua</string>
<key>ProgramArguments</key>
<array>
<string>/Applications/MyApp.app/Contents/MacOS/MyApp</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>StandardErrorPath</key>
<string>/dev/null</string>
<key>StandardOutPath</key>
<string>/dev/null</string>
</dict>
</plist>
Here's what each part does:
- Label — A unique identifier for this Launch Agent. Use a reverse-domain style name that matches your app (e.g.,
com.mycompany.myapp). - LimitLoadToSessionType — Setting this to
Aquameans the agent only loads during a graphical login session (i.e., when the user is sitting at the Mac with the GUI running). This is what you want for a PyQt app. - ProgramArguments — The path to the executable inside your
.appbundle. For a bundled macOS application, this is typically/Applications/YourApp.app/Contents/MacOS/YourApp. - RunAtLoad — When set to
true, this tells macOS to launch the program as soon as the agent is loaded (i.e., at login). - StandardErrorPath and StandardOutPath — Redirecting these to
/dev/nullprevents log files from piling up. During development, you could point these to actual files to capture output for debugging.
Save this file to:
~/Library/LaunchAgents/com.mycompany.myapp.plist
The filename doesn't technically need to match the Label, but keeping them consistent is a good habit — it makes the file easy to find and ensures you won't accidentally overwrite another agent's configuration.
After saving the file, log out and back in. Your app should launch automatically.
PyQt/PySide Office Hours 1:1 with Martin Fitzpatrick — Save yourself time and frustration. Get one on one help with your projects. Bring issues, bugs and questions about usability to architecture and maintainability, and leave with solutions.
Adding "Run at startup" to your PyQt app
Rather than asking users to manually create .plist files, you can let them toggle this from within your application. The approach is straightforward: write the .plist file to enable auto-start, and delete it to disable it.
Here's a complete working example that demonstrates this. The app shows a simple window with a checkbox to toggle the launch-at-login behavior:
import os
import sys
from PyQt6.QtWidgets import (
QApplication,
QCheckBox,
QMainWindow,
QVBoxLayout,
QWidget,
)
# Configuration — adjust these to match your app.
APP_NAME = "MyApp"
APP_BUNDLE_ID = "com.mycompany.myapp"
APP_EXECUTABLE = f"/Applications/{APP_NAME}.app/Contents/MacOS/{APP_NAME}"
PLIST_FILENAME = f"{APP_BUNDLE_ID}.plist"
LAUNCH_AGENTS_DIR = os.path.expanduser("~/Library/LaunchAgents")
PLIST_PATH = os.path.join(LAUNCH_AGENTS_DIR, PLIST_FILENAME)
PLIST_CONTENT = f"""<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>{APP_BUNDLE_ID}</string>
<key>LimitLoadToSessionType</key>
<string>Aqua</string>
<key>ProgramArguments</key>
<array>
<string>{APP_EXECUTABLE}</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>StandardErrorPath</key>
<string>/dev/null</string>
<key>StandardOutPath</key>
<string>/dev/null</string>
</dict>
</plist>
"""
def is_launch_agent_installed():
"""Check whether our Launch Agent .plist file exists."""
return os.path.isfile(PLIST_PATH)
def install_launch_agent():
"""Write the .plist file to ~/Library/LaunchAgents."""
os.makedirs(LAUNCH_AGENTS_DIR, exist_ok=True)
with open(PLIST_PATH, "w") as f:
f.write(PLIST_CONTENT)
def remove_launch_agent():
"""Delete the .plist file from ~/Library/LaunchAgents."""
if os.path.isfile(PLIST_PATH):
os.remove(PLIST_PATH)
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Launch at Login Demo")
self.checkbox = QCheckBox("Launch at login")
self.checkbox.setChecked(is_launch_agent_installed())
self.checkbox.toggled.connect(self.toggle_launch_at_login)
layout = QVBoxLayout()
layout.addWidget(self.checkbox)
container = QWidget()
container.setLayout(layout)
self.setCentralWidget(container)
def toggle_launch_at_login(self, checked):
if checked:
install_launch_agent()
else:
remove_launch_agent()
app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec()
When the user checks the box, the .plist file is written to ~/Library/LaunchAgents/. When they uncheck it, the file is removed. The next time they log in, macOS will (or won't) launch the app accordingly.
If you're new to building PyQt6 applications, see our tutorial on creating your first window and the guide on signals, slots and events to understand how the checkbox toggling works.
A few things to keep in mind
Adjust the executable path. The APP_EXECUTABLE variable needs to point to the actual binary inside your .app bundle. If you're using a tool like PyInstaller to package your application into a macOS .dmg, check where the executable ends up inside Contents/MacOS/.
The ~/Library/LaunchAgents folder might not exist. The os.makedirs(..., exist_ok=True) call in the example handles this, creating the directory if it's missing.
Using a consistent Label and filename means you can safely check whether the file exists, and delete it without worrying about affecting other applications.
If you previously used fbs to generate a .plist, note that the fbs-generated plist may not include the RunAtLoad key, and even adding it manually may not produce the expected result. Writing your own .plist from scratch (as shown above) is the more reliable approach.
Code signing and Gatekeeper. In testing, this approach works even without signing the app or disabling Gatekeeper. However, for distribution to end users, you should sign your application properly. Unsigned apps may behave differently depending on the macOS version and the user's security settings.
Purchasing Power Parity
Developers in [[ country ]] get [[ discount.discount_pc ]]% OFF on all books & courses with code [[ discount.coupon_code ]]Bring Your PyQt/PySide Application to Market
Specialized launch support for scientific and engineering software built using Python & Qt.