Python · Lesson 18 of 21

Virtual Environments and Project Setup

Set up Python projects the modern way: venv, pip, requirements files, pyproject.toml, uv for fast installs and lockfiles, and a clean src layout.

  • Intermediate
  • 16 min read
  • 4 objectives

Before this lessonLesson 17: Modules, Packages and pip

What you will learn

  • Explain why every project needs its own environment
  • Create and use environments with venv and pip
  • Describe a project in pyproject.toml
  • Manage dependencies and lockfiles with uv

Your Progress

0 of 21 lessons 0%

  • Lessons0 / 21
  • Completed0
  • Est. time left~ 5 hours

Create a free account to keep your progress on every device.

Tip: pressing Next marks this lesson complete automatically.

In the modules lesson you created a virtual environment and installed a package with pip. That is enough for a quick script. Real projects need more: a record of dependencies that teammates and servers can reproduce exactly, a place for project metadata, and settings for tools like pytest and linters. This lesson walks through the modern Python project setup, from plain venv to pyproject.toml and the fast uv tool.

Why environments exist

Without environments, every pip install goes into one shared place. Project A needs Django 4, project B needs Django 5, and whichever you installed last wins. Worse, installing into the system Python on macOS or Linux can break operating system tools, which is why modern distributions block it with an externally-managed-environment error.

A virtual environment is just a folder (conventionally .venv) containing a link to a Python interpreter and its own site-packages directory. Activating it puts that folder first on your PATH. You can see which environment is active from inside Python:

import sys

print(sys.prefix == sys.base_prefix)   # True means no venv is active
print(sys.version_info >= (3, 10))
Output
True
True

Inside an activated virtual environment, sys.prefix points at .venv and the first line prints False.

The classic workflow: venv and pip

This works everywhere Python is installed, with no extra tools. Use python -m pip rather than bare pip so you are certain which interpreter installs the package.

python3 -m venv .venv
source .venv/bin/activate          # Windows: .venv\Scripts\activate
python -m pip install --upgrade pip
python -m pip install "requests>=2.32" pytest
python -m pip list
python -m pip freeze > requirements.txt
deactivate

requirements.txt pins every installed package, including dependencies of dependencies. A teammate recreates the environment with python -m pip install -r requirements.txt. Always add .venv/ to .gitignore: environments are machine-specific and are rebuilt, never committed.

Version specifiers

When you list dependencies you choose how strict to be. Libraries should use loose ranges so they play well with others; applications should lock exact versions for reproducible deploys.

requests            # any version
requests==2.32.3    # exactly this version
requests>=2.32      # this version or newer
requests~=2.32.0    # compatible release: >=2.32.0, <2.33
requests>=2.32,<3   # a range

pyproject.toml: one file for the project

pyproject.toml is the standard configuration file for Python projects (PEP 518 and PEP 621). It holds the project name, version, supported Python versions and dependencies, the build backend, and settings for tools such as pytest, ruff and mypy. It replaces the older mix of setup.py, setup.cfg and scattered config files.

[project]
name = "stackcone-orders"
version = "0.1.0"
description = "Order processing service"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
    "httpx>=0.27",
    "pydantic>=2.8",
]

[project.optional-dependencies]
cli = ["rich>=13"]

[dependency-groups]
dev = ["pytest>=8", "ruff>=0.6", "mypy>=1.11"]

[project.scripts]
orders = "stackcone_orders.cli:main"

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[tool.pytest.ini_options]
testpaths = ["tests"]

[tool.ruff]
line-length = 100

dependencies are what your code needs at runtime. [dependency-groups] (PEP 735) holds development-only tools. [project.scripts] creates a command called orders that runs the main function. With this file in place, python -m pip install -e . installs your project in editable mode so code changes take effect immediately.

The src layout

A widely recommended structure puts your package inside a src/ folder. It forces tests to import the installed package rather than whatever happens to be in the current directory, which catches packaging mistakes early.

stackcone-orders/
  pyproject.toml
  uv.lock
  README.md
  .gitignore
  .python-version
  src/
    stackcone_orders/
      __init__.py
      cli.py
      models.py
  tests/
    test_models.py

uv: the fast all-in-one tool

uv, from Astral, has become the most popular modern Python project tool. Written in Rust, it replaces pip, venv, pip-tools, pipx and pyenv with one command that is typically 10 to 100 times faster. It creates the .venv for you, edits pyproject.toml when you add packages, and writes a cross-platform uv.lock lockfile with exact versions and hashes.

# install once (macOS/Linux; see docs.astral.sh/uv for Windows)
curl -LsSf https://astral.sh/uv/install.sh | sh

uv init stackcone-orders --package   # creates pyproject.toml and src layout
cd stackcone-orders
uv python pin 3.13                   # writes .python-version, downloads Python if needed
uv add httpx pydantic                # adds to dependencies, updates uv.lock
uv add --dev pytest ruff             # adds to the dev dependency group
uv run pytest                        # syncs .venv, then runs inside it
uv run orders                        # runs your project script
uv lock --upgrade-package httpx      # bump one dependency

uv run checks that the environment matches the lockfile before every command, so you rarely need to activate anything. A teammate who clones the repository runs uv sync and gets exactly the same versions. Commit both pyproject.toml and uv.lock.

uv also handles one-off tools and scripts: uvx ruff check . runs a tool in a throwaway environment, and uv pip install is a drop-in faster pip if you prefer the classic workflow.

A reproducible checklist

  • One environment per project, in .venv, ignored by git.
  • Declare direct dependencies in pyproject.toml with sensible ranges.
  • Commit a lockfile (uv.lock, or a pinned requirements.txt) for applications.
  • Pin the Python version (requires-python and .python-version).
  • Put tool settings in [tool.*] tables so the whole team shares them.

Recap

  • Virtual environments isolate each project's packages and protect the system Python.
  • python -m venv .venv plus python -m pip works everywhere; pip freeze pins versions.
  • pyproject.toml is the single source of project metadata, dependencies and tool config.
  • The src/ layout keeps imports honest and packaging mistakes visible.
  • uv manages Python versions, environments, dependencies and lockfiles quickly with uv add, uv sync and uv run.
// Write your solution here

Finished reading? Mark this lesson complete to track your progress.

Up next · Lesson 19JSON, Dates and the Standard LibraryRead and write JSON, work with dates and durations, and reach for collections and pathlib.