Python Starter Pack
Overview
This guide provides a modern, efficient approach to Python project management using uv
, a high-performance replacement for traditional tools like pip, pyenv, and poetry. It's designed for Python developers who want to streamline their development workflow and adopt current best practices for dependency management.
Prerequisites
- Basic familiarity with Python and command line
- Basic understanding of virtual environments and package management
- macOS, Linux, or Windows system
Choosing uv as a package manager
uv is a python project and package manager that replaces pip
, pyenv
, poetry
, and mostly conda
. It's faster, simpler, and better.
Why not conda?
You can have many anti-conda rants on the internet. It's bulky, it has complicated installation process that easily tricks you into installing the full anaconda suit with all the things you never need. Pragmatically, each time you create a new virtual environment, it installs a new python in that env, so if you have 5 different projects each using python 3.12.8, you will have 5 copies of python 3.12.8 installed. Why would you want that?
Why not pyenv + venv?
A more reasonable alternative, until Feb 2024, was using pyenv, which installs global versions of python that you can easily switch between, and then creating venv
using python native python -m venv venv
. Your workflow was:
So why uv?
uv basically mirrors workflow above, but makes everything faster.
As a bonus, installing uv is incredibly easy, just one curl script that is easily found in docs without requiring you to go through 10 accordeons and tabs.
Starting a new project
After creating and cd
into your project directory, you can run uv init
, which will create:
.python-version
- a simple text file specifying python version you chose when runninguv venv --python 3.12
. If a project has.python-version
specified, you can simply runuv venv
and it'll use (or install if that python is not installed already) python version specified in.python-version
.pyproject.toml
- a modern replacement tosetup.py
,requirements.txt
.main.py
- your first.py
file.
See python-template repo for an example of a filled pyproject.toml
.
Creating your package
Choosing when to structure your code as a package is ultimately a matter of preference. As of today, I prefer to create a package from the very beginning. Simply because:
- if it's a research project, I will release the code regardless of whether it truly is a standalone package (i.e. other people will use it in their projects as a dependency) just for reproducibility reasons
- given that
uv
usespyproject.toml
to enumerate dependencies already, it's a matter of a few extra lines to convert codebase into a package - having a central package removes the necessity to either explicitly add path to sys or use some complicated relative path imports. If you had these issues before, you know what I mean.
Why should I put my package under src/
You might notice that python-template repo or DirectMultiStep hides the package files under src/
directory. This is a good practice, because, if you have the following structure:
my-project/
├── pyproject.toml
├── README.md
├── my_package/
│ ├── __init__.py
│ └── module.py
└── script.py
when you install my_package
in development mode (e.g. uv pip install -e .
), the project root directory (my-project/
) is added to the python path. This means that any file in your project root can be accidentally imported as part of your package. For example, if you have a script.py
in your project root, it could be mistakenly imported as my_package.test
.
By placing your package under src/
, like this:
my-project/
├── pyproject.toml
├── README.md
├── src/
│ └── my_package/
│ ├── __init__.py
│ └── module.py
└── tests/
and configuring your pyproject.toml
accordingly, only the src/
directory is added to the python path during development installation. This clearly separates your package code from other project files (like configuration, tests, documentation, etc.) and prevents potential naming conflicts and accidental imports from the project root. It's a cleaner and more robust way to structure your project.
Installing Dependencies
Your package
Once you've created some package in src/
, all that remains is to add this to your pyproject.toml
:
Don't forget to activate your venv
Once you have your project set up, your venv should be pretty much always activated. Your IDE (VSCode, Cursor) should automatically recognize presence of .venv
folder and activate it in the terminal sessions. But if you open your repository in a standalone terminal window, you'll have to manually source .venv/bin/activate
.
After this, you can install your package in development mode uv pip install -e .
. The -e
specifies dev mode and effectively it means that any changes made to the source code in src/my_package
will be immediately available (i.e. you don't need to reinstall your package).
External Dependencies
Instead of running pip install package
you should use uv add package
. Using uv add
instead of pip install
results in:
package
added as dependency topyproject.toml
uv.lock
updated
You might find uv's docs helpful if you need to specify specific (including platform-specific) source for the package.
pyproject.toml and uv.lock
At this point, you might wonder -- what is uv.lock
and how is it different from pyproject.toml
?
In short:
pyproject.toml
is a soft specification of minimal version requirements of top-level packagesuv.lock
is a strict and full specification of all packages installed in current venv.
As an example, if you uv add
a package, say, SciPy, pyproject.toml
will add scipy>=1.15.2.
, i.e. a spec of minimally required version. However, SciPy itself depends on NumPy, so NumPy will be installed in the venv and reflected in uv.lock
, but not in pyproject.toml
.
In some sense, uv.lock
is similar to what you'd get from pip freeze
. Why do we need both pyproject.toml
and uv.lock
?
pyproject.toml
is useful to keep track of which packages you intentionally added. If you want to update, say, SciPy to 1.16, you can updatepyproject.toml
and runuv lock --upgrade
. If SciPy depends on 10 other packages, you shouldn't be required to manually update dependencies of all those packages, which is why it's all handled for you byuv.lock
.uv.lock
is useful because it allows any other user to recreate your local environment exactly. You never need to manually modify this file, as long as you useuv add
,uv remove
to handle dependencies.
See uv locking and syncing page for more details.
uv.lock gives you confidence that your code will always work
Basically, once you have a working codebase, even if you stop working on it, you (or any other human) can come back to it at any point in the future, and he'll be able to run it without any issues. Your code will work even if numpy updated 10 times in the meantime or some other package stopped being maintained. Fun fact: lock files have been a standard in web dev for almost 15 years, you might be familiar with package-lock.json
and yarn.lock
.
Dependency Groups
By default, every package you uv add
appears as:
However, you can also have optional dependency groups:
which you can install by:
uv pip install -e ".[dev]" # will install main deps and dev deps
uv pip install -e ".[web]" # will install main + web deps
uv pip install -e ".[dev,web]" # will install main + dev + web deps
which is useful when, say, 2 people are working on the project and only one of them develops the web-facing code. There's no reason for the other person to have all web-related dependencies in his venv, so they can be separated into a dep group.
Migrating from Traditional Tools
From pip + venv
If you're currently using pip with venv, migration is straightforward:
-
Install uv: Follow the installation instructions above
-
For existing projects:
-
Convert to modern structure:
- Create
pyproject.toml
usinguv init
- Move dependencies from
requirements.txt
topyproject.toml
- Run
uv pip install -e .
to install in editable mode
- Create
From conda
For conda users, the transition requires a few additional steps:
-
Export your conda environment:
-
Identify pure Python packages from conda-packages.txt
-
Create new project with uv:
-
Install required packages using
uv add
-
For conda-specific packages (like MKL-optimized numpy), refer to package documentation for pip-compatible alternatives
Summary
Key Takeaways
uv
provides a faster, simpler alternative to traditional Python tooling- The src/ layout with pyproject.toml offers a clean, modern project structure
- Lock files ensure reproducible environments across team members and time
- Dependency groups help manage optional features efficiently