This gist is the "do it once, stop suffering" setup for macOS (Apple Silicon) where:
- Homebrew's default
python3is too new for your project (ex: Python 3.14 breaks/doesn't have stable wheels for stuff like Pygame yet) - macOS still has
/usr/bin/python3(often 3.9.x) and it silently wins when Homebrew isn't configured how you think - you want a project-local venv that is always Python 3.12, with
pipinstalls going into the venv (no PEP 668 drama) - you also want IDLE/Tkinter to work on 3.12 (optional)
This documents the exact failures I hit and the final working config.
I hit these recurring issues:
-
macOS defaulted to
/usr/bin/python3(3.9.6) Even with Homebrew installed,python3 -Vwas still3.9.6because Homebrew'spython3shim wasn't in play. -
PEP 668
externally-managed-environmenterror inside a "venv" That error shows up whenpipthinks you're trying to install into a managed system environment (Homebrew Python), not a venv.Root cause was split-brain Python:
pipsometimes resolved to the venvpythonresolved to Homebrew/global (or vice-versa)- and/or
~/.zshrcre-sourced mid-session shuffled PATH
-
Aliases for
python/python3broke venv behavior Aliases override PATH resolution. If you aliaspython=python3.12, you can accidentally bypass.venv/bin/pythoneven when a venv is active. -
Homebrew's "python" vs "python@3.12" behavior is confusing
brew install pythoninstalls the current Homebrew Python (in my case: 3.14.x) and creates/opt/homebrew/bin/python3brew install python@3.12installs versioned Python 3.12, but it does NOT necessarily create/opt/homebrew/bin/python3
So you can have Python 3.12 installed and still have
python3defaulting to/usr/bin/python3or Homebrew 3.14. -
IDLE failed in the 3.12 venv: "IDLE can't import Tkinter" Tkinter is not a pip thing. It's a build / linkage thing for the base interpreter. Fixed by installing the matching Homebrew Tk package for 3.12.
Homebrew wants python3 to mean "latest stable" (3.14 now). Pygame often lags on brand new Python minors.
So instead of trying to force global python3 to 3.12, you:
- install
python@3.12 - create each project venv using the explicit 3.12 interpreter
- inside venv, run
python -m pip ...(or.venv/bin/python -m pip ...) - never use
--break-system-packagesfor normal work
Result: global Python can be 3.14 (or Apple 3.9), and your project still runs on 3.12.
brew install python@3.12Confirm it exists:
/opt/homebrew/opt/python@3.12/bin/python3.12 -VIf you want python -m idlelib to work with the 3.12 interpreter:
brew install python-tk@3.12Verify:
/opt/homebrew/opt/python@3.12/bin/python3.12 -c "import tkinter; print(tkinter.TkVersion)"This makes Homebrew paths consistent and avoids repeatedly stacking PATH entries.
~/.zprofile
# Homebrew environment (Apple Silicon default path)
eval "$(/opt/homebrew/bin/brew shellenv)"Why:
~/.zprofileruns for login shells;~/.zshrcruns for interactive shells. Usingbrew shellenvis cleaner than manually prepending/opt/homebrew/binrepeatedly.
Key rule: do NOT alias python or python3. Ever.
If you want a shortcut, alias a named command like py312, not python.
alias python='python3.12'
alias python3='python3.12'Why it's bad:
- it overrides the venv's
.venv/bin/pythoneven after activation - you end up running global Python with venv pip (or the reverse)
- you'll see PEP 668 errors and/or installs going to the wrong place
Put this in ~/.zshrc.
It guarantees:
- venv is created using Homebrew 3.12
- installs are done using the venv interpreter, not PATH guessing
- no accidental interaction with
/usr/bin/python3or Homebrew 3.14
# Create/refresh a venv using Homebrew Python 3.12, then optionally install deps.
# Usage:
# venv312 # create .venv (or reuse) + activate + optional deps install
# venv312 --recreate # delete .venv first
venv312() {
local py="/opt/homebrew/opt/python@3.12/bin/python3.12"
local recreate=0
if [[ "${1:-}" == "--recreate" ]]; then
recreate=1
fi
if [[ ! -x "$py" ]]; then
echo "ERROR: Homebrew Python 3.12 not found at: $py" >&2
echo "Fix: brew install python@3.12" >&2
return 1
fi
if (( recreate )); then
rm -rf .venv
fi
# Always (re)create the venv using the explicit 3.12 interpreter if missing.
if [[ ! -d .venv ]]; then
"$py" -m venv .venv || return 1
fi
# Activate for your prompt, env vars, etc.
source .venv/bin/activate || return 1
# From here on, NEVER rely on `python`/`pip` resolution. Always pin to the venv.
local vpy=".venv/bin/python"
if [[ ! -x "$vpy" ]]; then
echo "ERROR: venv python not found at: $vpy" >&2
echo "Fix: rm -rf .venv && $py -m venv .venv" >&2
return 1
fi
"$vpy" -m pip install -U pip >/dev/null || return 1
# Install deps if common files exist (in priority order)
if [[ -f requirements-dev.txt ]]; then
"$vpy" -m pip install -r requirements-dev.txt || return 1
elif [[ -f requirements.txt ]]; then
"$vpy" -m pip install -r requirements.txt || return 1
elif [[ -f pyproject.toml ]]; then
# Only works if the project is set up for it; harmless if it isn't.
"$vpy" -m pip install -e . || return 1
else
echo "No requirements.txt / requirements-dev.txt / pyproject.toml found. Skipping dependency install."
fi
}Put this in ~/.zshrc:
pywho() {
echo "VIRTUAL_ENV=${VIRTUAL_ENV:-<none>}"
type -a python python3 pip 2>/dev/null || true
command -v python >/dev/null && python -c "import sys; print(sys.executable)"
command -v python >/dev/null && python -m pip -V
}Use it whenever something smells off:
pywhosys.executablepoints into.venv/...python -m pip -Vpoints into.venv/...
If either points to /usr/bin or /opt/homebrew/... outside .venv, you're not actually using the venv interpreter.
That error is pip protecting Homebrew's system-managed Python.
If you see it, you are not installing into your venv (even if your prompt shows (.venv)).
Common causes I hit:
pythonorpython3aliases bypassed the venv interpreter- re-sourcing
~/.zshrcwhile a venv is active reordered PATH and broke resolution - running
pipdirectly instead ofpython -m pipmade it easier to hit the wrong pip
- Run
pywho - If split-brain exists: remove aliases, stop re-sourcing
.zshrc, recreate venv withvenv312 - Never "solve" this by globally enabling
--break-system-packages
Using
--break-system-packagesis a last resort for global installs. It is NOT the normal path for dev.
I repeatedly ran:
source ~/.zshrc...while in (.venv).
This can:
- prepend Homebrew paths again
- reintroduce aliases / functions
- break the venv's PATH ordering
Best practice: after changing shell config, open a new terminal tab/window.
Even with python@3.12 installed and linked, I observed:
/opt/homebrew/bin/python3was missingpython3fell back to/usr/bin/python3(3.9.6)
That's expected-ish:
python@3.12is versioned and doesn't necessarily provide the unversionedpython3shimbrew install pythonprovides/opt/homebrew/bin/python3(but it's currently 3.14.x)
Takeaway: don't rely on python3 being 3.12 globally. Use the explicit 3.12 binary to create venvs.
This wasn't theoretical — it bit me. Pygame (and other compiled/wheeled deps) often lag behind brand-new Python minors. The result can be:
- wheels not available yet for the new version
- crashes, missing dependencies, build failures
- "works on 3.12, breaks on 3.14" situations
Practical policy: if you depend on native extensions (Pygame, numpy stacks, etc.), stick to a conservative Python version for projects and upgrade when your deps catch up.
python -m idlelib
# ** IDLE can't import Tkinter. Your Python may not be configured for Tk. **brew install python-tk@3.12Verify:
/opt/homebrew/opt/python@3.12/bin/python3.12 -c "import tkinter; print(tkinter.TkVersion)"It's tied to the base Python build + its linked Tk libraries. Once installed for 3.12, any venv created from that interpreter can use it.
From project root:
rm -rf .venv # only when you want a clean rebuild
venv312 # creates + activates + installs deps
pywho # sanity check
python main.py- Check interpreter + pip:
pywho- If you see
/usr/bin/python3or Homebrew 3.14 and you expected venv:
- deactivate + re-run
venv312 --recreate
- If you see PEP 668 error:
- you are installing into a managed Python (not the venv)
- fix resolution; do NOT "solve" it with global break flags
- If IDLE complains about Tkinter:
brew install python-tk@3.12I hit these milestones in order:
- removed
.venv, created a venv withpython3 -m venv .venv(accidentally using Apple 3.9) - tried pip installs → got PEP 668 error → temporarily used
--break-system-packages(worked, but wrong) - discovered
virtualenvcommand wasn't installed (irrelevant;venvis fine) - discovered alias + re-sourcing
.zshrccaused split-brain Python/pip - removed python aliases and stopped executing debug commands inside
.zshrc - realized
python@3.12doesn't create/opt/homebrew/bin/python3 - accepted global
python3might be 3.14 while projects are pinned to 3.12 - fixed IDLE by installing
python-tk@3.12
- Keep global Homebrew
python3as whatever Homebrew wants (3.14+). - Pin each project to the Python you actually trust (3.12) via
venv312. - Never alias
python/python3. - Use
pywhowhenever something smells off. - Don't re-source
~/.zshrcwhile a venv is active (start a new shell instead). - Only use Tkinter/IDLE if you install the matching
python-tk@X.Y.
That setup is stable, boring, and doesn't require fighting Homebrew upgrades.