orchard
mirrorEvery site I host, in one repo, along with the Cloudflare Tunnel and Caddy that front them. It's all Go, Vite, and SQLite, and it runs on a desktop at home with nothing listening on an inbound port.
blogbuncaddycloudflare-tunneldockergogolanghomelabhtml-templatemonorepoself-hostedseosqlitestatic-sitetypstuptime-monitoringviteweb-analytics
1---
2title: Managing a Python project with uv in 2026
3slug: managing-a-python-project-with-uv-in-2026
4date: 2026-04-05
5publish_date: 2026-04-05
6tags: coding
7description: How I set up and manage Python projects in 2026 with uv and ruff, which is two tools and one config file instead of the pile I used to keep around.
8cover_image: uv-terminal.webp
9---
10
11Setting up a Python project used to mean juggling a handful of tools for package management, Python versions, and virtual environments, and for a long time that meant pipenv, pyenv, and Black for me. I've since moved on and in 2026 I use `uv` for project management and `ruff` for linting and formatting, which comes out to two tools and one config file for the whole thing.
12
13## Installing uv
14
15[uv](https://docs.astral.sh/uv/) is a single binary that replaces pip, pipenv, pyenv, and virtualenv all at once. It's written in Rust by the Astral folks and it's extremely fast, fast enough that I stopped noticing installs entirely.
16
17```shell
18curl -LsSf https://astral.sh/uv/install.sh | sh
19```
20
21## Starting a new project
22
23Instead of hand creating a `Pipfile` or a `requirements.txt` you just run `uv init` and you get a project scaffold with a `pyproject.toml` ready to go.
24
25```shell
26uv init my-project
27cd my-project
28```
29
30That gives you something like this:
31
32```
33my-project/
34├── .python-version
35├── README.md
36├── main.py
37└── pyproject.toml
38```
39
40There's no `Pipfile`, no `setup.cfg`, and no `requirements.txt` in there, everything those used to hold lives in `pyproject.toml` instead.
41
42## Managing Python versions
43
44You don't need pyenv anymore since uv will download and manage Python versions for you.
45
46```shell
47uv python pin 3.13
48uv python install
49```
50
51This writes `3.13` to `.python-version` and installs it if you don't already have that version sitting around, which is about all there is to it.
52
53## Adding dependencies
54
55Adding a dependency updates your `pyproject.toml` and your `uv.lock` in one step.
56
57```shell
58uv add flask requests
59uv add --dev pytest ruff
60```
61
62When you clone the project on another machine or set up CI, `uv sync` installs everything from the lockfile.
63
64## Running things
65
66`uv run` executes inside the project's virtual environment without you needing to activate anything first, so I've stopped typing `pipenv shell` or `source .venv/bin/activate` before I can do anything.
67
68```shell
69uv run python main.py
70uv run pytest
71```
72
73## Linting and formatting with Ruff
74
75I used to run Black, Flake8, and isort separately but [Ruff](https://docs.astral.sh/ruff/) handles all of that on its own now, and it's from the same team that makes uv so it's just as fast. The formatter is designed as a drop-in replacement for Black with near-identical output, so switching over probably won't mean a massive reformatting commit across your codebase.
76
77```shell
78uv run ruff check .
79uv run ruff format .
80```
81
82`ruff check` handles linting and import sorting and `ruff format` handles the code formatting, and you configure both of them in `pyproject.toml`:
83
84```toml
85[tool.ruff]
86line-length = 88
87target-version = "py313"
88
89[tool.ruff.lint]
90select = ["E", "F", "I", "UP"]
91
92[tool.ruff.format]
93quote-style = "double"
94```
95
96`E` and `F` are the pycodestyle and pyflakes rules that Flake8 used by default, `I` is isort-compatible import sorting, and `UP` is pyupgrade which modernizes your syntax automatically. You can browse the full [rule list](https://docs.astral.sh/ruff/rules/) and add whatever else makes sense for your project, it's up to you how strict you want to be.
97
98## The full pyproject.toml
99
100Here's roughly what a complete `pyproject.toml` ends up looking like, and it replaces the `Pipfile`, `Pipfile.lock`, `.flake8`, and `.isort.cfg` I used to have scattered across all of my projects.
101
102```toml
103[project]
104name = "my-project"
105version = "0.1.0"
106description = ""
107readme = "README.md"
108requires-python = ">=3.13"
109dependencies = [
110 "flask",
111 "requests",
112]
113
114[dependency-groups]
115dev = [
116 "pytest",
117 "ruff",
118]
119
120[tool.ruff]
121line-length = 88
122target-version = "py313"
123
124[tool.ruff.lint]
125select = ["E", "F", "I", "UP"]
126
127[tool.ruff.format]
128quote-style = "double"
129```
130
131That's about all you need to get a Python project off the ground in 2026. If you're still juggling a handful of separate tools and config files then give uv and ruff a try, they've worked well for me so far.