I've adopted a new (to me) pattern for my Python projects to make them easier to hack on using uv run. I'm using a PEP 735 dependency group called dev to declare my development dependencies - in particular pytest - such that running uv run pytest executes the tests for my project without me having to even think about setting up a virtual environment first.
Here's the pattern I'm using. I start by creating a new library using uv init --lib like this:
mkdir my-new-library
cd my-new-library
uv init --libThis creates a pyproject.toml file like this:
[project]
name = "my-new-library"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
authors = [
{ name = "Simon Willison", email = "...@gmail.com" }
]
requires-python = ">=3.10"
dependencies = []
[build-system]
requires = ["uv_build>=0.9.15,<0.10.0"]
build-backend = "uv_build"It also creates a src/my_new_library/__init__.py file with a hello() example function.
Next, I add pytest as a development dependency using this command:
uv add --dev pytestDoing so adds a new section to the end of that pyproject.toml file:
[dependency-groups]
dev = [
"pytest>=9.0.1",
]This also creates the virtual environment in .venv and uv.lock file, but we don't need to think about those.
Then I create a test:
mkdir tests
echo 'from my_new_library import hello
def test_hello():
assert hello() == "Hello from my-new-library!"' > tests/test_my_new_library.pyNow I can run that test site using:
uv run pytestThe dev dependency group is a special case for uv run - it will always install those dependencies as well such that commands like pytest can work correctly.
When you package a project for distribution the dev dependencies will not be automatically installed for users of your package.
That [build-system] section is crucial, because it tells uv that the directory should be treated as a "package". This means that when uv run executes it installs the current directory as an editable package in the virtual environment.
Removing [build-system] and then rm -rf .venv to delete the virtual environment results in the following error when trying to run uv run pytest:
________________ ERROR collecting tests/test_my_new_library.py _________________
ImportError while importing test module '/private/tmp/my-new-library/tests/test_my_new_library.py'.
Hint: make sure your test modules/packages have valid Python names.
Traceback:
/opt/homebrew/Caskroom/miniconda/base/lib/python3.10/importlib/__init__.py:126: in import_module
return _bootstrap._gcd_import(name[level:], package, level)
tests/test_my_new_library.py:1: in <module>
from my_new_library import hello
E ModuleNotFoundError: No module named 'my_new_library'
=========================== short test summary info ============================
ERROR tests/test_my_new_library.py
!!!!!!!!!!!!!!!!!!!! Interrupted: 1 error during collection !!!!!!!!!!!!!!!!!!!!
=============================== 1 error in 0.07s ===============================
Adding the [build-system] section back in gets rid of this error.
There's an alternative to having a [build-system] section, which is to add this to the pyproject.toml file instead:
[tool.uv]
package = trueI had Claude Code investigate this and it found this section of code explaining what's going on:
pub fn is_package(&self, require_build_system: bool) -> bool {
// If `tool.uv.package` is set, defer to that explicit setting.
if let Some(is_package) = self.tool_uv_package() {
return is_package;
}
// Otherwise, a project is assumed to be a package if `build-system` is present.
self.build_system.is_some() || !require_build_system
}So there's no need to use that [tool.uv] section if you already have a [build-system] section.
If you're still using regular pip in your CI scripts you'll need to ensure the dev dependency group is installed like this:
pip install . --group devProjects that use this pattern become a whole lot easier for other people to hack on. I first used this for my datasette-extract package, with the result that checking out and running the tests is now a case of running just the following commands:
git clone https://github.com/datasette/datasette-extract
cd datasette-extract
uv run pytestNo need to think about virtual environments or development dependencies - this just works.
Building a distributable wheel of the project is a one-liner as well:
uv buildThis creates two files:
dist/datasette_extract-0.2a0-py3-none-any.whl
dist/datasette_extract-0.2a0.tar.gz
The .tar.gz file should contain everything including the tests - the .whl file should contain just the non-development Python code.
Created 2025-12-02T20:55:17-08:00 · Edit