Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Setting up for development

Slides

Before you begin

You’ll want to have the following tools installed:

These tools will be handy too, but they can be all installed via uv tool install so you don’t need to pre-install them:

Low level concepts

(Or how not to break your computer)

The topics in this session are there just to give you an understanding of what’s going on. We’ll introduce high level abstractions in the next section!

Virtual environments

The first thing you should know when packaging is how to install stuff. There are three options:

LocationProsCons
System site-packages/...Works from anywhereCan break your machine
User site-packages~/.local/...User permissionCan break other Pythons
Virtual environment.venv/ (common)Many!More effort

A system or user install sounds nice, but you can’t install incompatible packages, they can break your system, you can’t control what each project needs, you can end up not knowing what your requirements are, it’s hard to update, etc.

The standard solution is a virtual environment. It places files inside a folder with a name you choose (.venv in the project folder is the standard choice) that looks like this:

.venv
├── .gitignore      # These make sure tools know not to include/archive
├── CACHEDIR.TAG    # this folder
│
├── bin
│   ├── activate    # "activation" scripts for different shells (bash default)
│   ...
│   ├── python      # Symlinks to your local Python install
│   ├── python3
│   └── python3.14
│
├── lib
│   └── python3.14
│       └── site-packages  # This is where installed libraries go
│           ...
│
└── pyvenv.cfg      # Special file telling Python this is a virtual environment

When Python runs, it checks to see if there’s a pyvenv.cfg above it. If there is, it knows it is in a virtual environment (venv) and reads site-packages from there. There are two ways to use it:

Direct usage
Activation
.venv/bin/python ...

The . at the start (most shells support source as well) allows the activation script to set environment variables, something a normal application could not do. It sets PATH (and VIRTUALENV, but that is informational) so things inside the virtual environment’s bin are at the beginning of the PATH.

To create one of these, you have several options:

Create a virtual environment
venv
virtualenv
uv
python3 -m venv .venv

This is the slowest, but it’s built in![1]

We will be using uv, which can do a lot of this for us.

Requirements

Now that you know how to make virtual environments, how should you install stuff into them? If the environment is active, then installers will target it. If an environment is not active, uv pip install will check for a .venv folder, and will target that by default (which is why it doesn’t need to install pip in the venv).

But a virtual environment is meant to be expendable. You should be able to delete it and recreate it any time. So instead of manually installing, you want to list packages in some format:

Project (app)

These are for making a virtual env. They don’t affect libraries.

  • requirement.txt: Classic, very old format

    • Basically a list of args to pass to pip install

  • requirements.in: Manual locking

    • You make a locked requirements.txt from this file

  • Lock file: Versions are pinned exactly

  • dependency-groups: Multiple collections of packages

Package (library)

These are for libraries.

  • dependencies: The minimum requirements to install your package

  • optional-dependencies: Sets of extra requirements that can be requested on install

Most libraries also have developer environments, which follows the “Project (app)” patterns for things like tests and documentation.

Locked dependencies means that every dependency is fully specified, ideally with a SHA to make sure users get exactly the versions you used. Using a lockfile is a great way to recover a virtual environment exactly on another machine. However, dependencies cannot be locked for libraries; it would be a problem if your two favorite libraries could not be installed together in one environment because they conflicted on pins!

Summary

So far, we have discussed:

venv
Virtual environment that isolates dependencies
requirements
A list of packages to install
lock files
Fully pinned set of packages

This might seem simple, but creating, activating, installing, and locking are all separate steps with different incantations and different tools.

Solution to Exercise 1

Assuming a unix-like system and bash:

uv venv                 # defaults to .venv
uv pip install cowsay   # defaults to .venv
.venv/bin/cowsay        # run without activation is fine
rm -r .venv             # venvs can be removed

High level packaging

Apps

Persistent apps

Let’s break up applications and libraries. An application is something you install and run, but (assuming you use virtual environments) never needs to be installed with other unrelated things (apps that support plugins are okay). Generally, you won’t import an app inside Python, you’ll run it from the command line (or a graphical interface, etc).

This special property allows us to do something interesting. Imagine we:

  1. Made a venv somewhere

  2. Installed our application in it

  3. Put just that application somwehere on our PATH

Since we never need to import it, we can get away without activation. This is exactly what pipx (pip for executables) and uv tool do!

Use uv tool install to install apps. Use uv tool list to see what you’ve installed. uv tool upgrade to upgrade, and uv tool uninstall to remove them. (See uv tool --help).

Solution to Exercise 2

Assuming a unix-like system and bash:

uv tool install cowsay
cowsay
uv tool uninstall cowsay

Single use apps

We can do one better if we have something we don’t want to run all the time. The install and run steps can be combined! This is such a common need that uv comes with a separate CLI uvx that does this. Running this:

uvx cowsay

Will make a venv, download the app, then run a command with the same name in the venv. If you run it again, it will recreate the venv if it’s over a week old.

With this, you basically have all of PyPI at your fingertips, and you don’t have to remember to update things too!

Single file scripts

Another thing we can do is single-file scripts. They look like this:

simple.py
# /// script
# dependencies = ["numpy"]
# ///

import numpy as np

if __name__ == "__main__":
    print(np.array([1, 2, 3]))

When you run it:

uv
pipx
uv run single.py

The dependencies will be downloaded into a temporary venv.

Projects (websites, etc)

These are very similar to libraries (below); the key difference is you should always commit your lockfile (vs. being optional for libraries).

How you specify your dependencies depends a bit on the tool you are using, since it is not standardized like libraries are. But you can always use [project] and make it an unpublishable library by adding this:

pyproject.toml
[project]
classifiers = [
  "Private :: Do Not Upload",
]

Libraries

A library is something that can be shared with others. The key feature is that it must be able to share an environment with whatever else the user of the library needs. If a user is expected to import your code, it’s a library.

Later sections will explain a lot more about packages, so let’s just present a basic pyproject.toml:

pyproject.toml
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "example"
version = "0.1.0"

Notice the [build-system] section; this tells tools how to build the package into something you can distribute and install. There are quite a few build backends; the one above uses hatchling.

There are a lot of places to put dependencies; here’s an expanded version with annotations:

pyproject.toml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
[build-system]
requires = ["hatchling"]  # 1
build-backend = "hatchling.build"

[project]
name = "example"
version = "0.1.0"
dependencies = [] # 2

[project.optional-dependencies]
extra = []  # 3

[dependency-groups]
dev = [] # 4
build-system.requires
Requirements that are installed when building distributions. This is your build-backend, and anything else require to assemble your package from source. These are not available at runtime for users. Noted with # 1 above.
project.dependencies
Libraries that must always be installed to use your package. Any installation of your package will also install these. Noted with # 2 above.
project.optional-dependencies
This is a table with arbitrary keys. When a user is installing your package, they can add [extra] to install the list of dependencies named extra. These will not neccisarly be present if the user didn’t request then. Also known as “extras”. These are part of the public package metadata. Noted with # 3 above.
dependency-groups
This is a table with arbitrary keys. Unlike project.optional-dependencies, these do not become part of the package metadata; you must have the pyproject.toml file to install them. They also do not require you install the package. Commonly used for development dependencies, like tests, docs, coverage, and the like. Noted with # 4 above.

A quick comparison:

b-s.rp.dp.o-dd-g
Public metadata
Always installed
Named groups
Can be independently installed

High level project management with uv

When dependency-groups were introduced, uv made a brilliant decision: if there’s a group named dev, it is installed by default when using uv run. This enables the following file:

pyproject.toml
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "example"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["numpy"]

[dependency-groups]
dev = ["pytest"]

to be all you need to use uv run. For example,

uv run pytest

will:

If you edit the dependencies, then the lockfile and .venv will be updated when you uv run again.

Any command works, so uv run python starts up python, if you have command line apps you can use uv run ..., etc.

Solution to Exercise 3

Assuming a unix-like system and bash:

git clone https://github.com/pypa/packaging
cd packaging
uv run pytest
Footnotes
  1. Distributions may strip it out and make it a separate intallable package.