Changing the Python version means telling the OS, the shell, and the project which interpreter to use, and each layer has its own lever. The OS has a default Python (usually Python 3 on Linux, Python 2 on legacy macOS). The shell has a PATH that decides which python or python3 the user gets. The project has a .python-version file, a pyvenv, a requirements.txt, or a pyproject.toml that pins the version. The three layers are independent, and the three layers are the part the developer has to know to make a version change stick.
The reason “change python version” is its own question and not just “use python” is that the version is the part of the toolchain that breaks first. A project that needs Python 3.11 and the developer has 3.9 is a project that is going to fail with a SyntaxError or an ImportError on the first run. The fix is in the version manager, the system package, or the project config — and the developer has to know which lever to pull.
Table of contents
- The short version
- The three tools that actually change the version
- The five places the version is set
- The seven gotchas that quietly break a version change
- The way pyenv, venv, and pip interact
- The three patterns a team should standardize on
- FAQ
The short version
There are three tools a developer can use to change the Python version. pyenv installs and switches between Python versions on macOS and Linux. The system package manager (apt, brew, dnf) installs a system-wide Python that the developer can make the default with update-alternatives. The shebang line (#!/usr/bin/env python3.11) tells a specific script which interpreter to use. The three tools are not interchangeable, and the right tool depends on the developer’s goal — install a new version, switch between versions, or pin a version for a single script.
The three tools that actually change the version
The three tools are the ones the developer reaches for first, and the three are the ones that cover 90% of real use cases. The other tools (conda, asdf, nix, the Python launcher for Windows) are useful, but the three are the floor.
pyenv — the version manager. pyenv is a tool that installs multiple Python versions side by side, and lets the developer switch between them on a per-shell, per-directory, or per-project basis. The tool is the right answer for a developer who needs to run multiple projects on different Python versions, and the tool is the right answer for a developer who wants to test code against multiple Python versions. The tool is supported on macOS and Linux, and there is a pyenv-win port for Windows.
# Install a specific Python version
pyenv install 3.12.1
# Set the global default
pyenv global 3.12.1
# Set the version for the current directory (creates .python-version)
pyenv local 3.11.7
# Set the version for the current shell
pyenv shell 3.10.13
The system package manager — the system default. On Linux, the system package manager (apt on Debian/Ubuntu, dnf on RHEL/Fedora, pacman on Arch) installs a Python version that is available system-wide. The installation is at /usr/bin/python3 (or /usr/local/bin/python3), and the version is the one the OS considers the default. The tool is the right answer for a developer who wants the system to have a single, stable Python, and the tool is the right answer for a deployment environment (a Docker image, a server).
# Debian/Ubuntu
sudo apt install python3.11
# macOS with Homebrew
brew install [email protected]
# RHEL/Fedora
sudo dnf install python3.12
The shebang — the per-script pin. The shebang line at the top of a script (#!/usr/bin/env python3.11) tells the OS which interpreter to use when the script is run as an executable. The shebang is the right answer for a script that needs a specific version, and the shebang is the right answer for a project that is distributed as a script. The shebang is also the right answer for a Docker image that has multiple Python versions and needs each script to use the right one.
#!/usr/bin/env python3.11
print("Running on Python", __import__('sys').version)
The three tools cover 90% of real use cases. The developer should pick the tool that matches the goal — pyenv for switching, the package manager for the system default, the shebang for per-script pinning. The tools are not interchangeable, and the developer who reaches for the wrong one is the developer who is going to spend an hour debugging a version that “should be” 3.11 but is actually 3.9.
The five places the version is set
The version is not just one setting. The version is set in five different places, and the developer has to know all five to make a change stick.
/usr/bin/python3 and /usr/bin/python. The system Python. The version is the one the OS package manager installed, and the version is the one that runs when the user types python3 without any path manipulation. The fix for a wrong system Python is to install the right version with the package manager, and to set the right version as the default with update-alternatives (on Debian-flavored systems) or alternatives (on RHEL-flavored systems).
$PATH. The shell’s search path. The PATH decides which python or python3 the user gets when they type the bare command. A PATH that has /usr/local/bin before /usr/bin will pick up a Homebrew or pyenv-installed Python before the system Python. The fix is to check which python3 and python3 --version to see what the shell is actually picking up.
pyenv shims. The pyenv tool puts shims in ~/.pyenv/shims/ that intercept calls to python and python3 and route them to the version pyenv has set. The shims are the way pyenv makes the version change transparent. The fix is to make sure ~/.pyenv/shims is at the front of the PATH.
VIRTUAL_ENV and pyvenv.cfg. The Python virtual environment. A python -m venv myenv creates a myenv/ directory with a pyvenv.cfg that pins the Python version, and a bin/python that is a copy of (or symlink to) the version that was used to create the venv. The fix is to activate the venv (source myenv/bin/activate) before running Python, and to make sure the venv was created with the right Python.
.python-version. The pyenv-managed file that pins the version for a specific directory. The file is created by pyenv local <version>, and the file is read by pyenv when the developer enters the directory. The file is the right answer for a project that needs a specific version, and the file is the right answer for a project that should be reproducible across machines.
The five places are the floor. There is also pyproject.toml’s requires-python field (a hint to pip, not a hard requirement), the setup.py’s python_requires (similar), and the runtime.txt on Heroku (a Heroku-specific convention). The five are the ones the developer should know first.
The seven gotchas that quietly break a version change
A short, opinionated list of gotchas that have actually broken real version changes. None of them are dramatic. They are the boring ones.
The shell has cached the old path. A developer who installs a new Python and then runs python3 --version in a shell that was already open is going to see the old version. The fix is to open a new shell, or to hash -r (bash) or rehash (zsh) to clear the command cache.
The PATH is set in the wrong file. A developer who adds pyenv init to ~/.bashrc but uses zsh is a developer whose pyenv shims are not in the path. The fix is to set up pyenv for the actual shell the developer is using (~/.zshrc for zsh, ~/.bashrc for bash).
The system Python is being used instead of the pyenv Python. A developer who runs pip install and gets a Permission denied error on a system directory is a developer whose pip is bound to the system Python, not the pyenv Python. The fix is to use python -m pip install (which uses the active Python’s pip) or to set up pyenv correctly so the shims come first in the PATH.
The virtual environment was created with the wrong Python. A developer who runs python -m venv myenv with the system Python, then activates the venv and installs packages, has a venv that is bound to the system Python. The fix is to recreate the venv with the right Python: ~/.pyenv/versions/3.11.7/bin/python -m venv myenv, then source myenv/bin/activate.
The shebang is wrong. A developer who runs chmod +x script.py and ./script.py and gets a “command not found” error is a developer whose shebang is wrong. The fix is to make the shebang point to an interpreter that exists, and to use env so the PATH is respected (#!/usr/bin/env python3.11 is more portable than #!/usr/bin/python3.11).
The project has a requires-python that is too tight. A developer who installs a Python version newer than the project’s requires-python = ">=3.10,<3.12" is a developer whose pip install is going to fail. The fix is to either install the right Python version, or to relax the requires-python in pyproject.toml.
The CI is using the wrong version. A developer who fixes the version locally and then watches the CI fail with a different version is a developer whose CI config is not pinned. The fix is to pin the version in the CI config (actions/setup-python@v5 with python-version: '3.11.7', or the equivalent in the CI system), and to use the same version locally.
The way pyenv, venv, and pip interact
The three tools (pyenv, venv, pip) are independent, and the three are the ones a real Python project uses. The interactions are the part the developer has to know to make the project reproducible.
pyenv chooses the interpreter. pyenv decides which python and python3 the shell sees. The shim intercepts the call, looks up the version in the pyenv config (global, local, shell), and dispatches to the right interpreter. The result is that the developer can have multiple Python versions installed, and the active one is a function of the directory the developer is in.
venv creates an isolated environment. python -m venv myenv creates a directory with a pyvenv.cfg that pins the Python version (the one used to create the venv), a bin/python that is a copy of (or symlink to) the chosen Python, and a lib/python3.X/site-packages/ that holds the packages. The venv is isolated from the system Python and from any other venv, so packages installed in one venv do not affect another.
pip installs packages into the active environment. pip install <package> installs the package into the active environment — the venv if one is activated, the system Python if not. The packages are installed into lib/python3.X/site-packages/, and the packages are available only to the active Python.
The three together are the typical Python project setup. The flow is “use pyenv to pick the Python version, use venv to create an isolated environment, use pip to install packages.” The three are independent, and the developer has to know all three to make the setup reproducible.
The three patterns a team should standardize on
A short, opinionated list of patterns a team should standardize on. The patterns are the ones that make a Python project reproducible across machines, across CI, and across time.
The pyenv + .python-version + venv pattern. The team standardizes on pyenv for version management, .python-version for per-project pinning, and venv for per-project isolation. The flow is: pyenv install 3.12.1, pyenv local 3.12.1, python -m venv .venv, source .venv/bin/activate, pip install -r requirements.txt. The pattern is the right answer for a team that needs to run multiple projects on different Python versions.
The pyproject.toml + pip-tools + venv pattern. The team standardizes on pyproject.toml for dependency declaration, pip-tools for lock file generation, and venv for per-project isolation. The flow is: write pyproject.toml, run pip-compile to generate requirements.txt, create a venv, install from requirements.txt. The pattern is the right answer for a team that needs reproducible builds across machines and CI.
The Docker + python:3.12-slim pattern. The team standardizes on Docker for the production environment, with a specific base image (python:3.12-slim or python:3.11-slim). The flow is: write a Dockerfile that uses the right base image, copy the code in, install the dependencies, set the entrypoint. The pattern is the right answer for a team that ships a Python service to production.
The three patterns are not exhaustive — there are also patterns with conda, with Nix, with Poetry, with uv — but the three are the ones the developer should know first. The team should pick one pattern and stick with it, and the team should not mix patterns within the same project.
How this fits the rest of the stack
A Python project rarely lives in isolation. The project usually has a database, an API, a worker, and a static site. The platform that handles the Python project should make the rest of the stack feel like part of the same conversation.
The services layer is the part of the platform that runs the long-lived Python API. The database layer is the part that holds the data the API reads and writes. The static layer is the part that hosts the dashboard the developer uses to inspect the data. The environment variables are the part that holds the secrets the Python project reads at runtime.
A Python project on a platform where the service, the database, the storage, and the secrets are all in the same place is a project the team is going to be able to operate. A Python project on a platform where each piece is in a different console is a project the team is going to spend the first hour just opening the right tab.
For a team that wants to see the full cost of the project before it commits, the RunxBuild hosting calculator shows the line items together. The API, the database, the storage, the worker, the bandwidth — each one is a separate number, and the team’s mental model for the platform is the sum of those numbers.
FAQ
How do I change the default Python version on my system?
The right answer depends on the OS. On macOS, use Homebrew (brew install [email protected] and then brew link [email protected] --force). On Debian/Ubuntu, use update-alternatives to point /usr/bin/python3 at the right binary. On macOS or Linux, the cleanest answer is pyenv, which lets you switch versions per directory without touching the system Python.
What is pyenv and how does it work?
pyenv is a Python version manager. It installs multiple Python versions side by side, and uses shims (intercept scripts in ~/.pyenv/shims/) to route calls to the right version based on the pyenv config (global, local, or shell). The shims are added to the front of the PATH, so python and pip automatically pick up the right version for the current directory.
How do I pin the Python version for a single project?
Use pyenv local 3.12.1 to create a .python-version file in the project directory. The file is read by pyenv when you cd into the directory, and the shims route to the right version. For Docker, use the python:3.12-slim base image. For pip, the requires-python field in pyproject.toml is a hint but not a hard requirement.
Why does my virtual environment still use the old Python version?
The venv was probably created with the old Python. The fix is to delete the venv and recreate it with the right Python: ~/.pyenv/versions/3.12.1/bin/python -m venv .venv, then source .venv/bin/activate. The pyvenv.cfg in the venv pins the Python version, and the venv cannot be “upgraded” to a different version.
How do I run a script with a specific Python version?
Use the shebang line: #!/usr/bin/env python3.11 at the top of the script. Then chmod +x script.py and ./script.py. The shebang uses env to look up the right Python in the PATH, so the script is portable across machines. For a one-off command, use python3.11 script.py directly.
Should I use pyenv or conda?
Both work, and the choice depends on the use case. pyenv is lightweight, focused on Python versions, and ideal for web development. conda is heavier, manages non-Python dependencies (C libraries, R, Julia), and is ideal for data science. The developer should not mix the two in the same project.
How do I set the Python version in CI?
Most CI systems have a setup action for Python. On GitHub Actions, use actions/setup-python@v5 with python-version: '3.12.1'. On GitLab CI, use the python:3.12.1 Docker image. The CI version should match the local version (or the version pinned in .python-version), and the CI config should be committed to the repo.