Environments and Workspaces
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.venvexplicitly (often unnecessary;uv syncanduv runcreate 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.pyOutput:
(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 venvThat 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.pyOutput:
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.pyOutput:
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.pyOutput:
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 syncoruv venv. Run withuv 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.pyprintsFalsefor.venv, stop and useuv run.
Try this
- Run
uv run python where.py, then trypython where.pywithoutuv runif you have another interpreter. Compare the two lines. - Delete
.venv(notpyproject.toml). Runuv syncthenuv run python shift.py. Confirm it comes back. - Read
.python-version. Change nothing until a later part asks you to.