Contents
  1. What Actually Causes the Break
  2. Why Deleting and Reinstalling Is a Symptom, Not a Fix
  3. The Proper Solution: pyenv and a Lockfile
  4. What You Can Do Now
← All posts

Returning to a Python Project on Mac After Months

Coming back to a Python project on Mac after six months often means a broken environment. Understanding why it breaks and how to prevent it is more useful than deleting everything and starting over.

You return to a Python project after six months or a year. You run the project and nothing works. The virtual environment fails to activate, imports resolve to the wrong versions, or Python itself throws an error about a missing interpreter. The fastest fix most people reach for is to delete the virtual environment and reinstall everything from scratch. That works, but it does not address what broke or why it will break again.

What Actually Causes the Break

On macOS, most developers install Python through Homebrew. Homebrew installs Python into a versioned path, something like /opt/homebrew/opt/python@3.11/bin/python3.11. When you create a virtual environment, Python records the absolute path to that interpreter inside the pyvenv.cfg file in your .venv directory. That path is what the virtual environment uses every time it activates.

Homebrew upgrades packages automatically during brew upgrade or when a formula that depends on Python is updated. When Python moves from 3.11 to 3.12, the path changes. The virtual environment still points to the old path, which no longer exists. The environment is broken, not because your project changed, but because the interpreter it was built against was moved or removed by an unrelated system operation.

This is documented behaviour. Homebrew explicitly states that Python may be upgraded at any time, and it recommends using a version manager for project-level stability rather than relying on the Homebrew-managed binary directly.

Why Deleting and Reinstalling Is a Symptom, Not a Fix

Recreating the virtual environment and running pip install -r requirements.txt restores a working state, but it reinstalls the environment against whatever Python version Homebrew currently provides. Six months from now, the same upgrade cycle will break it again in exactly the same way.

There is also a subtler problem. If you did not use a lockfile, reinstalling from requirements.txt does not guarantee you receive the same package versions as before. Packages that specified loose version constraints will resolve to whatever is newest at the time of install. The project may work, but it is not the same environment.

The Proper Solution: pyenv and a Lockfile

The two-part fix is a Python version manager and a lockfile.

pyenv manages Python versions independently of Homebrew. It installs each Python version into its own isolated directory under ~/.pyenv/versions/ and uses lightweight shim executables to intercept python and pip commands. When you run python in your terminal, the shim checks which version is active and routes the command to the correct binary.

The important behaviour for project stability is the .python-version file. Running pyenv local 3.11.9 inside a project directory writes that exact version to a .python-version file. Every time you enter that directory, pyenv activates that specific version automatically. When Homebrew upgrades its own Python, nothing changes for your project because your project is not using Homebrew’s Python. It is using a pyenv-managed binary that does not move.

# Install pyenv (if not already installed)
brew install pyenv

# Install a specific Python version
pyenv install 3.11.9

# Pin that version to your project
cd your-project
pyenv local 3.11.9

# Create the virtual environment using the pinned version
python -m venv .venv
source .venv/bin/activate

The .python-version file should be committed to version control. Anyone cloning the project, including yourself six months later, can run pyenv install to get the right version before creating the environment.

That said, pyenv is an additional tool to install and maintain. If you are already using Poetry, it can handle the Python version side of this problem on its own without pyenv.

Poetry’s approach is to let you explicitly tell it which Python binary to use for a project’s environment. When you run poetry env use, Poetry creates or recreates the virtual environment using that specific interpreter and records the Python version constraint in pyproject.toml under the python field. From that point forward, poetry install will refuse to run against a Python version that does not satisfy the declared constraint, which surfaces the version mismatch immediately rather than silently producing a broken environment.

# Tell Poetry which Python to use for this project
poetry env use python3.11

# Install dependencies using that environment
poetry install

The Python version is recorded in pyproject.toml like this:

[tool.poetry.dependencies]
python = "^3.11"

Commit pyproject.toml and poetry.lock together. When you return to the project after months, poetry install reproduces the exact environment, same Python constraint, same package versions, from those two files alone. If your system Python no longer satisfies the constraint, Poetry will tell you immediately rather than letting the project silently fail at runtime.

This is the approach I use personally. It keeps the toolchain to one tool, Poetry, rather than layering pyenv on top of it.

What You Can Do Now

If you have an existing Python project on Mac, check whether these two things are in place:

# Check if a Python version is pinned
cat .python-version

# Check if a lockfile exists
ls poetry.lock uv.lock 2>/dev/null

If neither exists, add them now while the environment is still working, before the next Homebrew upgrade makes the question urgent. Pin the Python version with pyenv, generate a lockfile with Poetry or uv, and commit both files. The next time you return to the project after months away, the environment will be reproducible from a single command rather than a process of elimination.

← All posts