Managing Python Versions and Virtual Environments on Linux

Managing Python on Linux is easier when you separate three jobs: maintaining the interpreter supplied by your distribution, choosing an interpreter for development, and keeping each project’s packages apart. Problems start when one installation is expected to do all three.

A practical default is to leave system Python alone, create a virtual environment for every project, and add a version manager only when you need another Python release. Record enough information to rebuild those environments rather than treating them as permanent installations.

This guide covers that workflow, including venv, uv, pyenv, dependency records and common failures. For the surrounding editor, compiler and Git setup, use the broader guide to setting up a Linux development workstation.

Python Versions and Virtual Environments Solve Different Problems

A Python interpreter runs your code. Its version determines which language features and standard library behaviour are available. A virtual environment gives a project its own installed Python packages while using a chosen base interpreter.

These are separate decisions. Two projects can use the same Python release but require different versions of a library. They need separate environments, not necessarily separate interpreter installations. Conversely, creating another environment from the same interpreter does not give you a different Python release.

Consider an existing service that runs on Python 3.13 and a second application being tested on Python 3.14. Install both interpreters without replacing the distribution’s Python, then create an environment for each application from the appropriate interpreter.

Which tool handles each part of a Python setup?
Task Suitable approach What it does not replace
Maintain distribution Python The Linux package manager A project’s dependency record
Separate project packages venv with pip An interpreter installation tool
Manage interpreters and project dependencies together uv Operating system maintenance
Install and select several Python releases pyenv, combined with project environments A dependency lockfile
Install standalone Python command line applications pipx Libraries inside a development project

Choose the smallest arrangement that meets the project’s requirements. Installing several managers before creating your first environment usually adds more decisions than it removes.

Keep the Distribution’s Python Under Distribution Control

Treat the Python installation managed by your Linux distribution as operating system infrastructure. Do not replace its executable, redirect its system symlinks to a development interpreter, or use administrator privileges to force project packages into it.

The EXTERNALLY-MANAGED marker formalises this boundary. Packaging tools can refuse installation outside a virtual environment when an interpreter belongs to another package manager. The Python packaging specification for externally managed environments defines that behaviour and the associated override mechanism.

An externally-managed-environment error therefore does not usually mean pip is broken. It means the requested installation targets a protected interpreter. Change the destination: use a project environment for libraries, an application manager for standalone tools, or a distribution package where that is appropriate.

Do not delete the marker or make --break-system-packages part of your normal development routine. The flag’s name is doing a reasonable job of describing the risk.

Keep operating system packages and Python project packages on separate maintenance tracks. The guide to APT, DNF and Pacman covers the distribution side of that arrangement.

Create One Virtual Environment per Project

Choose the interpreter before creating the environment

Start with the project’s documented Python requirement, not whichever interpreter happens to answer to the shortest command name. Check its development documentation, deployment configuration and existing automation. For a shared repository, ask whether the team already has an established environment manager.

Inspect both the interpreter version and its location. A version number alone can hide the fact that your terminal, editor and application launcher use different installations.

Use the distribution’s interpreter when it satisfies the project. Install another interpreter when compatibility requires it. There is no benefit in adding version switching to a project that never switches versions.

Create the environment beside the source, not around it

Use a project directory containing your source files and a separate environment directory, conventionally called .venv. Invoke Python’s venv module through the interpreter you selected, supplying that directory as the destination.

The resulting environment contains its own package installation area and interpreter entry point. Python’s venv documentation covers creation, activation and the relationship with the base interpreter.

Keep application code outside .venv. That separation makes rebuilding safe: deleting an environment should remove replaceable software, not yesterday’s work. Also check that Git excludes the environment directory rather than assuming every tool or Python release has handled it.

If the directory appears missing in a graphical file manager, check its hidden files setting. The leading dot follows the naming convention covered in the guide to Linux files and directory structure.

Activate for convenience, verify for certainty

Activation adjusts the current shell’s search path so that the environment’s programs take precedence. Use the activation script intended for your shell; Bash and Fish do not use identical activation files.

Activation is convenient, but it is not required. You can address the environment’s interpreter directly through its path, such as .venv/bin/python. For unattended work, use an absolute path rather than relying on a terminal session having been prepared beforehand.

After activation, check where Python and pip resolve. Both should belong to the intended environment. A prompt containing the project name is useful feedback, but it is not proof that every application launched from that terminal uses the same interpreter.

When command resolution looks wrong, investigate aliases, shell configuration and search path order before reinstalling Python. The Linux terminal commands guide provides the surrounding shell basics.

Install through the environment you intend to use

Run package installation through the chosen environment’s interpreter, using pip as a module, rather than trusting an unrelated pip executable elsewhere on the machine. This keeps the installation destination tied to the interpreter that will run the project.

Install the project’s declared dependencies before adding anything else. Then run its tests or a small representative task. A successful package installation confirms that files were installed; it does not confirm that the application works.

Do not share one environment between unrelated projects to save a little disk space. That saving becomes expensive when upgrading one application changes another.

Use uv When You Want a Combined Workflow

uv can manage Python installations as well as project environments and dependencies. It suits projects that want those tasks handled through one tool rather than separate interpreter and package management routines.

Its interpreter selection supports version requests, executable paths and project version files. It can also download a missing compatible interpreter unless automatic downloads are disabled. Managed CPython installations use builds from Astral’s python-build-standalone project; see uv’s Python version management documentation for selection and installation behaviour.

For a team, agree whether development should use tool-managed interpreters or an existing installation. On a controlled workstation, automatic downloads may conflict with local policy. Make that decision explicit rather than leaving each developer to interpret it differently.

A .python-version file records the requested development interpreter. It is not a substitute for declaring which Python versions the application supports. One describes the local choice; the other describes compatibility.

If you adopt uv’s project workflow, make it the normal route for dependency changes. Mixing manual package additions with managed synchronisation makes it harder to tell which packages belong in the project and which were temporary experiments.

Use pyenv When Interpreter Selection Is the Main Requirement

pyenv focuses on installing and selecting Python versions. It places small forwarding executables, called shims, in the shell’s search path and routes commands to the selected installation. Selection can apply to a shell session, a project directory or the user account.

Most Python releases installed through pyenv are built from source, so Linux installations need suitable build dependencies. The official pyenv documentation covers installation, shell setup and version selection.

Choose pyenv when you want independent control over interpreter installations, need build configuration choices, or already maintain projects around it. Pair the selected interpreter with a separate environment for each project.

Changing pyenv’s selection does not rebuild an existing virtual environment. If an environment was created with another interpreter, create a replacement rather than expecting the version selector to convert it.

Avoid giving uv and pyenv overlapping responsibility without a reason. They can coexist, but document which tool installs interpreters and which one creates project environments. Otherwise, the next troubleshooting session starts with a small archaeological dig through shell configuration.

Record Dependencies So the Environment Can Be Rebuilt

An environment is an installed result. The repository needs a description of how to produce that result again.

Keep three questions separate: which packages the project directly requires, which resolved package versions were selected, and which interpreter should run them. Those records serve different purposes, even when one tool manages all three.

Requirements files and installed package snapshots

A requirements file can describe packages for pip to install. How repeatable that installation will be depends on its contents and the installation process; the filename alone makes no promise.

pip’s freeze operation reports installed packages in requirements format. It does not calculate a lockfile or perform dependency resolution, a distinction stated in the pip freeze documentation.

Use an installed package snapshot as a record, not an explanation of design intent. If you experimented with several libraries in the same environment, review the result before committing it. Otherwise, abandoned experiments become requirements for everyone else.

Lockfiles and controlled updates

A lockfile records resolved dependency choices. In uv’s project workflow, locking and environment synchronisation normally happen automatically. Exact synchronisation removes packages that are not selected for installation, and the locked mode rejects an outdated lockfile instead of updating it. These behaviours are documented in uv’s locking and syncing reference.

For an application using that workflow, commit the dependency declarations and lockfile together. Make dependency upgrades deliberate changes, review their differences, then run tests before accepting them.

Do not assume a lockfile describes the entire computer. Record external requirements such as database services, system libraries and hardware dependencies separately. Where those requirements need a more controlled runtime, consider the sibling guide to local development with Docker and Podman.

Keep Standalone Python Applications Separate with pipx

A library imported by your application belongs in that application’s environment. A standalone command line utility used across projects may belong elsewhere.

pipx creates a separate environment for each installed application and exposes its commands through a directory on your search path. The pipx application isolation documentation describes that arrangement.

This is useful for a personal utility you want available without activating a project. It is not a way to provide importable libraries to every project.

For tools that affect shared project results, such as a formatter or test runner, prefer a version recorded with the project. A convenient user installation should not accidentally determine how the team’s code is formatted or tested.

Point Editors, Notebooks and Services at the Right Environment

Editors have their own interpreter selection

Do not assume that activating a terminal changes an editor that was already running. Set the project’s interpreter explicitly and check the editor’s test and debugging configuration.

In VS Code, use the Python interpreter selector and choose the project environment. The selected environment supplies the interpreter for running and debugging unless configuration overrides it; Microsoft’s Python environment documentation covers selection and environment discovery.

After replacing an environment, recheck that selection. An editor can retain a path that no longer exists even though a newly opened terminal works correctly.

Notebook kernels are another interpreter choice

A notebook frontend and its Python kernel can run from different environments. Installing a library beside the frontend does not establish that the selected kernel can import it.

When a notebook needs a separate project interpreter, install ipykernel in that environment and register a clearly named kernel. The IPython instructions for kernels in separate environments describe this arrangement.

Name kernels by project and interpreter where useful, then remove obsolete registrations when retiring environments. Several menu entries named “Python” provide very little help during diagnosis.

Services should not depend on interactive activation

For background jobs, specify the intended interpreter path, working directory and required configuration explicitly. Do not make successful startup depend on a developer’s terminal activation or personal shell startup files.

Keep that service configuration with the deployment documentation. The guide to managing Linux services with systemd covers service setup beyond the Python environment itself.

Troubleshoot the Interpreter Before Reinstalling Packages

A package installs but cannot be imported

First compare the interpreter used for installation with the one running the application. Check the terminal, editor, notebook kernel or service separately, depending on where the failure occurs.

Record the interpreter path, Python version and package version before changing anything. If those do not match the intended setup, correct the selection first. Repeated installation into the wrong environment only produces a more thoroughly populated wrong environment.

Environment creation reports missing support

Some distribution installations require an additional package to provide virtual environment support. Follow the distribution’s error message and install the support package matching the interpreter you are using.

If the interpreter came from a version manager, investigate that installation rather than assuming a distribution package will repair it. Keep the ownership boundary clear.

Installation fails with permission errors

A project environment should normally be writable by the account that owns the project. Check whether an earlier administrator-level operation created files owned by root.

Correct the ownership problem or rebuild the environment as the intended user. Do not turn every later installation into an administrator operation. The guide to Linux users, groups and sudo access covers that distinction.

Compiled packages fail during installation or import

Not every failure is an environment selection problem. Compiled dependencies can introduce compatibility issues between Python, the installed package and other binary components.

For NumPy-related failures, check the reported Python and NumPy versions, environment variables and installation history before choosing a fix. The NumPy import and binary compatibility troubleshooting guide distinguishes setup failures from incompatible compiled extensions.

Preserve the first useful error and installation log. Avoid changing Python versions, package versions and system libraries all at once; that makes it difficult to identify what actually repaired the problem.

Upgrade and Rebuild Environments Deliberately

Treat a move between Python minor releases, such as 3.13 to 3.14, as a compatibility change. Create a fresh environment, install the recorded dependencies and test the application before changing the normal development or deployment path.

For patch updates, follow the interpreter manager’s supported procedure and verify the project afterward. Do not assume that updating the Linux distribution also updates interpreters installed under your home directory. Assign responsibility for those installations alongside your other Linux security update routines.

Before removing an interpreter, identify the environments and applications that depend on it. Keep a working recovery route until the replacement has passed its checks.

Recreate environments after moving a project to another location or machine rather than treating the environment directory as portable. Keep source, dependency records, configuration and irreplaceable data in your backup plan. A repeatable rebuild is more useful than a large archive of installed packages; the guide to Linux desktop backup and recovery covers the wider recovery process.

The maintenance test is straightforward: can a clean checkout become a working project using the documented interpreter choice and dependency records? If it can, the environment is doing its job. If success depends on packages nobody remembers installing, improve the records before adding another management tool.