Creating a virtual environment is one command. Using virtual environments correctly, consistently, across a team and its CI pipeline, is a set of conventions most tutorials skip. This lesson covers the practices that keep environments a non-issue instead of a recurring source of 'which Python am I even using' confusion.
1Naming, Activation, and .gitignore Conventions
python -m venv .venv ā creating the environment in a directory literally named .venv ā is a convention, not a technical requirement, but it's a strong one worth following: the leading dot follows Unix hidden-file conventions (keeping it out of casual ls output and most file pickers), and .venv specifically is what a large fraction of editors (VS Code, PyCharm) and modern tools (uv) auto-detect and use without any additional configuration.
Activation (source .venv/bin/activate on macOS/Linux, .venv\Scripts\activate on Windows) modifies your current shell's PATH so that python and pip resolve to the versions inside .venv rather than your system Python ā this is entirely a shell-session convenience; the environment itself is fully usable without ever 'activating' it, by directly invoking .venv/bin/python (or .venv\Scripts\python.exe) with its full path, which is exactly what CI pipelines and some tooling do instead of activating.
The non-negotiable convention: .venv/ must always be in .gitignore. It contains absolute, machine-specific paths baked into its activation scripts, platform-specific compiled binaries for any C-extension dependencies, and is trivially regenerable from pyproject.toml/the lock file ā committing it bloats the repository, breaks the moment it's checked out on a different OS or path, and directly contradicts the principle that reproducibility should come from the lock file, not from shipping the environment itself.
$ python -m venv .venv
$ source .venv/bin/activate # macOS/Linux
$ .venv\Scripts\activate # Windows
(.venv) $ python -m pip install -e ".[dev]"Local, disposable, regenerable ā never committed
2Modern Tooling: uv Manages the Environment For You
The traditional workflow ā manually run python -m venv .venv, manually activate it, manually remember to reactivate it in every new terminal tab ā is exactly the kind of repetitive ceremony modern tools have started automating away. uv, a fast, Rust-based Python package and project manager, ties environment creation directly to your project's declared Python version and lock file: uv venv creates .venv using the correct interpreter version, uv sync installs precisely what's in uv.lock into it, and uv run pytest executes a command *inside* that environment automatically, without requiring you to have activated anything in your current shell first.
This matters beyond convenience: it eliminates an entire category of 'wait, which Python am I actually running right now' confusion ā the classic case of forgetting to activate a venv in a fresh terminal, then wondering why pip install is trying to write to system site-packages, or why an import that should work is failing. uv run always resolves to the project's own isolated environment, deterministically, regardless of what's currently activated (or not) in your shell.
Teams adopting uv (or a similar tool like Poetry, which offers comparable environment-plus-dependency management) typically stop thinking about virtual environments as a separate manual step at all ā uv sync after cloning a repo, uv run <command> for everything else, is the entire day-to-day workflow, with the underlying .venv mechanics handled transparently.
# .gitignore
.venv/
__pycache__/
*.egg-info/
.pytest_cache/
.mypy_cache/
.ruff_cache/Runs inside .venv automatically ā no manual activate step
3pipx: A Different Job ā Global Tools, Not Project Dependencies
A per-project .venv answers 'what does *this specific project* need to run.' A separate, equally important need is 'I want to use ruff (or black, or a CLI tool I built) from any directory on my machine, without it polluting or being polluted by any particular project's dependencies.' Installing such a tool with a plain pip install --user ruff risks version conflicts between different tools' own dependencies, and pip install ruff into your system Python risks conflicting with your OS's own Python tooling on some platforms.
pipx solves this specific problem: pipx install ruff creates a dedicated, isolated virtual environment just for ruff (invisible to you, managed entirely by pipx), and exposes only the ruff executable itself on your system PATH ā you get the convenience of a globally-available command with the isolation guarantees of a venv, and zero risk of ruff's own dependencies conflicting with any project's dependencies, or vice versa.
The distinction to keep straight: use a per-project .venv (or uv/Poetry's equivalent) for a project's own runtime and dev dependencies, declared in that project's pyproject.toml. Use pipx for standalone command-line tools you want available everywhere, independent of which project directory you happen to be in ā they solve genuinely different problems and aren't interchangeable.
$ uv venv # creates .venv, using the Python version from pyproject.toml
$ uv sync # installs the exact locked dependencies into it
$ uv run pytest # runs INSIDE the venv automatically, no activation neededGlobal command, isolated environment ā separate from any project's .venv
4Step-by-Step Breakdown
You already know python -m venv .venv. This lesson is about the ten small conventions that separate that from a professional, team-wide workflow.
python -m venv .venv creates an isolated environment. The convention of naming it exactly '.venv' (dot-prefixed) is what makes tools auto-detect it.
.venv should ALWAYS be in .gitignore ā it's a local, disposable artifact, never something to commit or share.
Checkpoint: Should .venv/ ever be committed to version control?
- āNo, never ā it should always be in .gitignore
- āYes, to guarantee every teammate has an identical environment
Modern tools like uv create AND manage the venv automatically, tied directly to your lock file ā no manual activate step required for most commands.
pipx is for installing standalone CLI TOOLS (like ruff or a CLI you built) globally, each in its own isolated environment ā different job than a per-project venv.
Checkpoint: What is pipx for, compared to a per-project venv?
- āInstalling standalone CLI tools globally, each isolated from your projects' dependencies
- āThe exact same purpose as venv ā just a different command
With project structure, packaging metadata, dependency locking, and environment isolation all covered, you have the complete professional foundation ā next, we move into Object-Oriented Design principles that scale to real systems.
Check a Real Auto-Detected Env Name. Finish is_auto_detected_venv_name(): the dot-prefixed convention is what tools look for.
Level Up š
Advanced cheat sheets, SEO tricks, and interview prep for this topic.
Browser Support
Fully supported (via server-side Python execution).
Fully supported (via server-side Python execution).
Fully supported (via server-side Python execution).
Fully supported (via server-side Python execution).
Best Practices
Always add .venv/ (and other tool caches) to .gitignore at project creation
It's local, machine-specific, and fully regenerable from committed metadata ā committing it bloats the repo and breaks across machines/OSes with zero reproducibility benefit.
Use uv (or an equivalent) to tie environment management directly to your lock file
Manual venv creation and activation is a repeated, forgettable ceremony; uv sync / uv run collapses it into commands that are always correct, regardless of what's currently activated in your shell.
Frequent Bugs
Forgetting to activate the correct project's venv in a fresh terminal tab, then installing a package into the wrong environment (system Python, or a different project's venv) without noticing.
Adopt uv run (or an editor that auto-activates the correct venv per project) so commands always execute against the right environment deterministically, rather than depending on manual activation state.
Real-World Examples
Onboarding a New Developer in Under a Minute
A new team member clones a repository and needs to get a fully working, correctly isolated development environment with zero ambiguity about versions.
$ git clone https://github.com/acme/project.git
$ cd project
$ uv sync # creates .venv, installs EXACTLY what uv.lock specifies
$ uv run pytest # verifies the setup immediately, no manual activation