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))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
deactivaterequirements.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 rangepyproject.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 = 100dependencies 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.pyuv: 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 dependencyuv 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.tomlwith sensible ranges. - Commit a lockfile (
uv.lock, or a pinnedrequirements.txt) for applications. - Pin the Python version (
requires-pythonand.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 .venvpluspython -m pipworks everywhere;pip freezepins versions.pyproject.tomlis the single source of project metadata, dependencies and tool config.- The
src/layout keeps imports honest and packaging mistakes visible. uvmanages Python versions, environments, dependencies and lockfiles quickly withuv add,uv syncanduv run.
// Write your solution here
Finished reading? Mark this lesson complete to track your progress.
