These are the traps I hit building local AI infrastructure - daemons, menu bar apps, automation scripts - on macOS with system Python.

System Python Is 3.9

macOS ships Python 3.9 via Command Line Tools at /Library/Developer/CommandLineTools/usr/bin/python3. This matters because:

  • No match statements (3.10+)
  • No str | None union syntax (3.10+) - use from __future__ import annotations or Optional[str]
  • No tomllib (3.11+) - use tomli package instead
  • Some packages drop 3.9 support - pyobjc-core 12.0 was yanked for Python 3.9 (but still installs and works)

If your scripts need to run without a venv (e.g., LaunchAgent daemons), you’re stuck with 3.9 unless you install a separate Python.

pip Installs to User Site

pip install --user puts packages in ~/Library/Python/3.9/lib/python/site-packages/. This is the default when not in a venv. Key implications:

  • Packages here are available to all scripts using system Python
  • Don’t install system-wide dependencies into a project venv - they won’t be available to LaunchAgent scripts that use system Python
  • If a daemon script imports a package, it must be in the user site or the system path

LaunchAgents Can’t Access iCloud Drive

launchctl runs processes in a restricted environment that cannot access ~/Library/Mobile Documents/com~apple~CloudDocs/. Scripts referenced from iCloud-synced directories will fail silently.

Fix: Copy runtime files to a non-iCloud directory (e.g., ~/alcanah-daemon/) and point the LaunchAgent plist there. Use an install script that copies from iCloud source to the runtime location.

NSApplication Timing Trap

When building macOS menu bar apps with rumps/pyobjc:

  • NSApplication.sharedApplication() returns None before the run loop starts
  • Setting setActivationPolicy_(1) (to hide the Dock icon) at import time does nothing
  • Must defer to a timer callback after the framework initializes
# Wrong - NSApp is None at this point
app = NSApplication.sharedApplication()
app.setActivationPolicy_(1)  # AttributeError

# Right - defer to after run loop starts
def hide_dock_icon(_):
    NSApplication.sharedApplication().setActivationPolicy_(1)

rumps.Timer(hide_dock_icon, 0.1).start()

Broader Lesson

macOS system Python is fine for lightweight automation, but the version lag and environment restrictions create subtle traps. If you’re building anything beyond simple scripts, either pin a Python version via pyenv/mise or accept the constraints and code to 3.9’s limitations explicitly.