Three separate field notes here describe builds on a Mac going quietly wrong. Read side by side, they’re chapters of one story. If you build automation on a Mac - or ask an AI assistant to - this is the shelf to read first.
The Python you didn’t choose is 3.9
macOS ships Python 3.9 with its developer tools. Background jobs on a Mac often have to run without a virtual environment - so they run on that system Python. 3.9 is missing things modern code reaches for:
- no
matchstatements (3.10+) - no
str | Noneunion syntax (3.10+); useOptional[str]or the future-annotations import - no
tomllib(3.11+); use thetomlipackage instead - some packages have formally dropped 3.9, though several still install and work
A script an assistant writes in today’s idiom can be correct everywhere except on the machine it has to run on.
| Trap | Looks like | Check before it ships |
|---|---|---|
| Language age | Modern Python syntax fails only when the job runs later | Run the script with the exact Python the job will use |
| Package location | Imports work in the project shell, then fail in the background | Ask that Python where its user packages live |
| Framework timing | Code that is valid at the prompt returns nothing at startup | Delay app-framework calls until the event loop exists |
The table is the whole note in miniature: the local test, the background job, and the Mac runtime can all be different machines wearing the same face.
Where pip actually puts things
Outside a virtual environment, pip install --user lands packages in the user site under ~/Library/Python/3.9/. A background job running on system Python can only import what’s in that user site or on the system path.
Dependencies installed into a project’s virtual environment don’t exist as far as the job is concerned. The import error arrives at 3 a.m., in a log nobody’s watching.
The two traps with their own notes
The other two quirks earned full write-ups, so here they’re cross-references:
- Background services can’t reach iCloud Drive at all. A job whose code lives under
~/Documents(which iCloud quietly relocates) loads with status 0 and never runs. The silent-failure note has the fix: move the runtime files to a plain home folder. - Apple’s application object doesn’t exist until the event loop starts. Ask for it at the top of a file and Python hands you
None. The timing-trap note has the working pattern: a 0.1-second timer.
Take the checklist
Before a Python background job ships on a Mac, four questions:
- Does it run on Python 3.9 exactly, or does it pin its own Python?
- Are its dependencies importable by the Python that will actually run it?
- Does its code live outside iCloud-managed folders?
- Does anything touch Apple’s frameworks before the event loop is up?
Each one cost a builder most of an evening. I shelved them together so the next builder falls once and reads once.