The Python ecosystem has grown to the point where every task has several competing tools, so picking one when setting up an environment is hard. You often end up installing separate tools for interpreter versions, virtual environments, and dependency management, which makes you wish for an all-in-one solution.
This post lays out a day-to-day toolchain:
- Use
uvto manage Python versions, virtual environments, and dependencies. - Use Ruff for formatting, import sorting, and static checks.
- Use
pyproject.tomlfor project metadata and tool configuration. - Use VS Code + Pylance for completion, navigation, and type hints in the editor.
The first thing that draws people to uv is speed, but what matters more is that it consolidates capabilities once scattered across pip, pip-tools, virtualenv, pipx, pyenv, and various project management tools into a single command.
Installing uv
The official docs live at uv docs. The standalone installer doesn’t depend on an existing Python, which makes it the best first choice.
curl -LsSf https://astral.sh/uv/install.sh | shpowershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"Managing Python Versions
A common hidden problem in Python projects: the author runs 3.12 locally while CI or a teammate’s machine runs 3.10. Syntax, the standard library, type annotations, and dependency resolution can all differ as a result.# List available versionsuv python list# Install a specific versionuv python install 3.12# Pin the version for the current projectuv python pin 3.12
uv python pin writes to .python-version. From then on, when you run uv run, uv sync, and similar commands in the project directory, uv looks for an interpreter matching that version first; if none is found and downloads are allowed, it fetches one automatically.
You can also declare the supported Python range in pyproject.toml:[project]requires-python = ">=3.12"
.python-version and requires-python each cover a different layer:File / field Purpose .python-versionPreferred interpreter for the local working directory; about developer experience project.requires-pythonPython range the project claims to support; about package metadata and dependency resolution
In personal projects it’s fine to write both. For example, require >=3.12 for the project while pinning 3.12 or 3.13 locally.
Creating a Project
For new projects, start with uv init:uv init my-appcd my-app
If the current directory is already the project directory:uv init
Run the sample program:uv run python main.py
The first time you run a project command, uv creates .venv, resolves dependencies, and generates uv.lock. Compared with manually activating a virtual environment, uv run carries its own context: you are explicitly running inside the project environment, instead of depending on whether the current shell has the right environment activated.
For example:uv run python --versionuv run python -c "import sys; print(sys.executable)"
The second command should print the Python path inside the project’s .venv. That’s the most direct way to check the environment is correct.
Virtual Environments: To Activate or Not
uv supports two ways of working.
The first is to skip activation and prefix every command with uv run:uv run python main.pyuv run pytestuv run ruff check .
This works well for scripts, CI, and documentation, because each command carries its own context and doesn’t depend on shell state.
The second is to activate .venv and run commands directly:
source .venv/bin/activatepython main.pydeactivate.\.venv\Scripts\Activate.ps1python main.pydeactivateoverlay use .venv/bin/activate.nupython main.pydeactivateOn Windows the Nushell path is usually:
overlay use .venv/Scripts/activate.nuMy habit: activating in an everyday terminal is fine, but documentation and automation scripts should use uv run, so the state of my current shell never becomes part of the project’s build.
Adding, Updating, and Removing Dependencies
# Add a runtime dependencyuv add requests# Add dev dependenciesuv add --dev pytest ruff# Pin versionsuv add "ruff==0.8.0"uv add "fastapi>=0.115"# Remove dependenciesuv remove requestsuv remove --dev ruff# Show the dependency treeuv treeNote the difference between uv add and uv pip install:Command Best for Written to project dependencies? uv add <pkg>Project dependency management Yes, updates pyproject.toml and the lockfileuv pip install <pkg>Legacy pip workflows or ad-hoc environment ops Usually doesn’t touch project metadata uv run --with <pkg> <cmd>Running a command with a temporary package Not persisted to the project
For example, running a tool temporarily:uv run --with rich python -c "from rich import print; print('[green]hello[/green]')"
If the package is a long-term project dependency, skip the temporary command and uv add rich instead.
Syncing and Locking Dependencies
In collaborative projects, commit uv.lock. After pulling the project, others run:uv sync
uv sync brings .venv in line with the project declaration and the lockfile. Per the official CLI docs, it creates the project environment if it doesn’t exist, and by default removes packages that aren’t declared, which makes it better for reproducing environments than “install whatever’s missing.”
Common commands:# Update the lockfile onlyuv lock# Sync the environment from the lockfileuv sync# Sync, then run a commanduv run pytest
If you need to provide a requirements.txt for environments that don’t use uv, export one:uv export --format requirements-txt --output-file requirements.txt
The old pip-tools style also works:uv pip compile pyproject.toml -o requirements.txtuv pip sync requirements.txt
Which one to choose depends on the project’s boundaries:
- Internal development: prefer
uv.lock+uv sync. - Deployment platforms that only accept
requirements.txt: generate it withuv exportoruv pip compile. - Migrating legacy projects: keep
requirements.txtfor now, letuv piptake over for install speed, then migrate topyproject.tomlgradually.
VS Code Extensions
Recommended extensions:
PythonPylanceRuffPython DebuggerPython postfix completion
Each has its own job:Extension Main responsibility Python Interpreter selection, test running, debug entry Pylance Language server, navigation, completion, type checks Ruff Formatting, import sorting, lint diagnostics and fixes Python Debugger Debugging Python programs Python postfix completion Postfix completion for faster editing
After installing, pick the interpreter from the VS Code command palette:Python: Select Interpreter
Choose the project’s .venv. If you don’t see one, run this in the project root first:uv sync
Format and Fix on Save in VS Code
Putting the config in a project-level .vscode/settings.json is more reproducible than a global one. A baseline:{ "editor.formatOnPaste": true, "editor.formatOnSave": true, "editor.formatOnType": true, "[python]": { "editor.defaultFormatter": "charliermarsh.ruff", "editor.codeActionsOnSave": { "source.fixAll.ruff": "always", "source.organizeImports.ruff": "always" } }}
The effect: on save, Ruff formats the file, applies auto-fixable lint rules, and organizes imports.
If the project already uses Black, Microsoft’s Black Formatter extension works too. But for new projects, letting Ruff handle both formatting and linting means less configuration and more consistent speed.
Pylance Configuration
Pylance handles “understanding code as you edit.” Keep its boundary with Ruff clear:
- Ruff is a fast code quality tool: formatting, imports, common errors, style rules.
- Pylance is language intelligence: type inference, navigation, completion, find references.
A good starting point:{ "python.languageServer": "Pylance", "python.analysis.cacheLSPData": true, "python.analysis.autoFormatStrings": true, "python.analysis.autoImportCompletions": true, "python.analysis.completeFunctionParens": true, "python.analysis.typeCheckingMode": "basic", "editor.inlayHints.enabled": "offUnlessPressed", "python.analysis.inlayHints.callArgumentNames": "all", "python.analysis.inlayHints.functionReturnTypes": true, "python.analysis.inlayHints.pytestParameters": true, "python.analysis.inlayHints.variableTypes": true, "python.terminal.activateEnvInCurrentTerminal": true, "python.terminal.shellIntegration.enabled": true}
Common values for typeCheckingMode:Value Best for offYou only want completion and navigation, no type errors basicRecommended starting point for everyday projects strictStrongly typed projects, core libraries, long-term maintenance
Don’t turn on strict for every project right away. Strict type checking is valuable, but it also surfaces a lot of legacy issues. If the codebase has plenty of dynamic code, start with basic, keep new modules clean, and raise the bar gradually.
Ruff: Formatting and Static Checks
Ruff is a Python linter and formatter from Astral. Its lint rules cover the common functionality of Flake8, isort, and pyupgrade, and its formatter is a drop-in replacement for Black.
After adding it as a dev dependency, four commands cover daily use:uv add --dev ruff# Lint / auto-fixuv run ruff check .uv run ruff check . --fix# Format / check formatting without changing files (good for CI)uv run ruff format .uv run ruff format . --check
Ruff config can live in pyproject.toml, ruff.toml, or .ruff.toml. If the project already has a pyproject.toml, centralizing there is usually cleaner.[tool.ruff]line-length = 100target-version = "py312"respect-gitignore = trueindent-width = 4[tool.ruff.format]quote-style = "double"indent-style = "space"docstring-code-format = true[tool.ruff.lint]select = [ "E", # pycodestyle error "F", # pyflakes "I", # isort "UP", # pyupgrade "B", # flake8-bugbear "SIM" # flake8-simplify]ignore = []
A few notes from experience:
line-length = 100is comfortable on modern screens; teams also commonly use 88 or 120. What matters is consistency.target-versionshould match the Python versions the project supports. Ruff uses it to decide whether newer syntax rewrites are safe.- Don’t be too aggressive with
selectat first. Start withE/F/I/UP/Band expand once things are stable. - If you’ve already set
requires-pythonunder[project], Ruff can infer the target version from it; whentarget-versionis set explicitly, the Ruff config wins.
A pyproject.toml Example
pyproject.toml is the central configuration file of a modern Python project. It grew out of the ecosystem’s evolution after PEP 518, to reduce the sprawl of setup.py, setup.cfg, requirements.txt, and per-tool config files.
A typical project might look like:[project]name = "vnet"version = "0.1.0"description = "Virtual network playground"readme = "README.md"requires-python = ">=3.12"dependencies = [ "pydantic>=2.10",][dependency-groups]dev = [ "pytest>=8.3", "pytest-cov>=6.0", "ruff",]doc = [ "mkdocs-material>=9.5", "mkdocs-mermaid2-plugin>=1.1",][tool.ruff]line-length = 100target-version = "py312"respect-gitignore = trueindent-width = 4[tool.ruff.format]quote-style = "double"indent-style = "space"docstring-code-format = true[tool.ruff.lint]select = ["E", "F", "I", "UP", "B", "SIM"]ignore = [][tool.pytest.ini_options]addopts = "-ra"testpaths = ["tests"]python_files = ["test_*.py"]python_classes = ["Test*"]python_functions = ["test_*"]log_cli = truelog_cli_format = "%(asctime)s [%(levelname)s] %(message)s"log_cli_date_format = "%m.%d %H:%M:%S"
A few common pitfalls:
dependenciesis for runtime dependencies, the libraries your package actually needs to run.- Don’t mix dev dependencies like
devinto runtime dependencies, or deployment environments get heavier. - Avoid writing
pythonpathas an absolute path on your machine, such as/home/name/workspace/project. That config only works on your computer. Prefer a standard package layout, or set the path explicitly in the test command. - Ruff supports
indent-style = "tab", but spaces are far more common in the Python community. Unless the team explicitly requires tabs, don’t change it.
Daily Command Cheat Sheet
This set of commands covers a personal project from init to checks:# Create a projectuv init my-appcd my-app# Pin the Python versionuv python pin 3.12uv sync# Add dependenciesuv add requestsuv add --dev pytest ruff# Runuv run python main.pyuv run pytest# Check and formatuv run ruff check .uv run ruff check . --fixuv run ruff format .# Show dependenciesuv tree# Sync the environmentuv sync