Environments and Workspaces

Updated

September 8, 2026

Environments and Workspaces

A virtual environment is a folder (usually .venv) that holds this project’s interpreter and packages. uv run uses it without a separate “activate” step. The boring default is one project, one venv. A workspace is several packages sharing one lockfile — skip it until you actually have two installable packages in one repo.

Mental model

Python itself can be installed many times on one machine. A venv points at one of those plus the packages uv sync installed. Mixing projects in a single global site-packages is how httpx versions collide.

uv conventions:

  • .venv/ — the environment, local to the project, not committed.
  • .python-version — the pin (3.14), is committed.
  • uv venv — create .venv explicitly (often unnecessary; uv sync and uv run create it).
  • uv python pin 3.14 — write .python-version.
  • uv run … — run a command inside .venv.

A workspace ([tool.uv.workspace]) is for a repo that contains more than one package you build. A folder of scripts is not that.

Worked examples

Case 1: Pin, sync, run

In a project folder:

uv python pin 3.14
uv sync

.python-version is:

3.14

pyproject.toml can stay this small:

# pyproject.toml
[project]
name = "desk"
version = "0.1.0"
requires-python = ">=3.14"
dependencies = []

Save as where.py. It checks two things that should be true under uv run: the version is 3.14, and the interpreter path lives inside .venv.

# where.py
import sys
from pathlib import Path


def main():
    parts = Path(sys.executable).parts
    print(sys.version_info[:2])
    print(".venv" in parts)


if __name__ == "__main__":
    main()

Run:

uv run python where.py

Output:

(3, 14)
True

If the second line is False, you are not in the project venv — you ran a bare python from somewhere else.

Case 2: uv venv is optional

You can create the directory yourself:

uv venv

That makes .venv. Then uv sync installs into it, and uv run python shift.py uses it. Save as shift.py:

# shift.py
def main():
    print("shift open")


if __name__ == "__main__":
    main()

Run:

uv run python shift.py

Output:

shift open

You can “activate” with source .venv/bin/activate (or the Windows equivalent) so a bare python hits the venv. You do not need to. This book keeps using uv run so the command in the chapter matches the command at work.

Case 3: VIRTUAL_ENV is set when uv run is

Save as env_flag.py.

# env_flag.py
import os


def main():
    print(bool(os.environ.get("VIRTUAL_ENV")))


if __name__ == "__main__":
    main()

Run:

uv run python env_flag.py

Output:

True

That flag is how tools notice they are inside an environment. You still do not activate by hand for the examples.

Case 4: Workspaces, briefly

A workspace is a repo-level pyproject.toml that lists member packages:

# pyproject.toml
[project]
name = "desk-root"
version = "0.1.0"
requires-python = ">=3.14"
dependencies = []

[tool.uv.workspace]
members = ["packages/desk", "packages/desk-cli"]

Each member has its own pyproject.toml. One uv.lock covers all of them. That layout pays off when desk-cli depends on desk and you want to change both in one commit.

It does not pay off for this book’s scripts. Do not create packages/ because a blog post used the word “monorepo.” One folder, one pyproject.toml, one .venv.

Save as one_project.py and keep shipping that idea:

# one_project.py
def main():
    print("one project, one venv")


if __name__ == "__main__":
    main()

Run:

uv run python one_project.py

Output:

one project, one venv

The trap

One “global” environment for every side project, or copying .venv between machines (it has absolute paths; it will break). Another: a workspace of empty packages so the repo “looks professional.”

Activate scripts, PYTHONPATH hacks, and two venvs in one folder (venv and .venv) are how you run tests against the wrong install. Pick .venv, let uv own it, put it in .gitignore.

The boring rule

  • One project directory, one .venv, one .python-version (3.14).
  • Create it with uv sync or uv venv. Run with uv run.
  • Commit pyproject.toml, uv.lock, and .python-version. Do not commit .venv.
  • Do not share a venv across projects.
  • Do not start a uv workspace until you have two packages that must ship together.
  • If where.py prints False for .venv, stop and use uv run.

Try this

  1. Run uv run python where.py, then try python where.py without uv run if you have another interpreter. Compare the two lines.
  2. Delete .venv (not pyproject.toml). Run uv sync then uv run python shift.py. Confirm it comes back.
  3. Read .python-version. Change nothing until a later part asks you to.