Python virtual environments and packaging: a clean setup

One virtual environment per project keeps dependencies from clashing. Learn venv, requirements files and a minimal pyproject.toml.

A tangled set of packages beside one clean, fenced environment box

Two projects need two versions of the same library. If you install both globally, one of them breaks. A virtual environment (venv) fixes this: a folder with its own copy of the Python interpreter links and its own installed packages, for one project only. Every Python developer should create one per project, by habit. The venv commands below were run before publishing.

The everyday workflow

python3 -m venv .venv            # create the environment in the project folder
source .venv/bin/activate        # Windows: .venv\Scripts\activate
python -m pip install requests   # installs into .venv only
python -m pip freeze > requirements.txt
deactivate
  • .venv is just a folder. Delete it and recreate it any time; nothing is lost if you kept requirements.txt.
  • Use python -m pip, not bare pip. It guarantees that you use the pip of the Python you are running.
  • Add .venv/ to .gitignore. Never commit it.

Check that you are inside it

import sys
print(sys.prefix.endswith(".venv"), sys.base_prefix != sys.prefix)   # True when you are inside a venv
$ python3 -m venv venvdemo
$ ./venvdemo/bin/python -c "import sys; print(sys.prefix.endswith('venvdemo'), sys.base_prefix != sys.prefix)"
True True
$ ./venvdemo/bin/python -m pip list
Package Version
------- -------
pip     25.1.1
$ ./venvdemo/bin/python -m pip freeze | wc -l
0

A fresh environment holds only pip, so pip freeze lists nothing. That is the point: your project gets only what it asks for.

Record and rebuild

  • pip freeze > requirements.txt writes the exact versions installed. On another machine: python -m pip install -r requirements.txt.
  • Keep two files if you like: a short list of what you chose (requests) and the full frozen list.
  • Pin versions for applications (reproducible), but use ranges for libraries (so users can combine them).

Packaging in one minute

If other people, or you on another machine, should install your project, describe it in a pyproject.toml:

[project]
name = "invoice-tool"
version = "0.1.0"
requires-python = ">=3.10"
dependencies = ["requests>=2.31"]

[project.scripts]
invoice = "invoice_tool.cli:main"

[build-system]
requires = ["setuptools>=68"]
build-backend = "setuptools.build_meta"

That is configuration, not output; build it with python -m pip install . or python -m build on your machine to confirm. Put your code in a package folder (invoice_tool/) with an __init__.py. The [project.scripts] line makes an invoice command.

Common mistakes

  • Installing with the system Python (sudo pip install) and breaking operating-system tools. Use a venv.
  • Forgetting to activate it, then wondering why the import fails. Check which python.
  • Committing .venv or the __pycache__ folders.
  • Not pinning versions, so the app breaks a month later after a dependency update.
  • Using one environment for several projects.
  • Copying a venv folder to another machine: it contains absolute paths. Recreate it instead.

Main idea: one venv per project, python -m pip inside it, a requirements.txt to rebuild, and a pyproject.toml when you want it installable.


Programming roadmaps series, Part 3 (writing better Python): 1 of 5. Previous: Software testing basics: the test pyramid and what to test.

Keep reading