UV All the Way: Your Go-To Python Environment Manager
In 2025 the best way to manage Python projects is using uv. This tutorial helps you set up a modern Python project from scratch using uv.
Yep, correct spelling is uv, two lower-case letters.
The two most important things that uv manages for you is a project-specific virtual environment (venv), and all the project dependencies in that venv.
Let’s start creating stuff!
Install uv
If you haven’t already, install uv on your machine. Open a shell or terminal to run these commands:
On MacOS, make sure you have Homebrew installed, then run
brew install uvto install uv.On Linux, run
curl -LsSf https://astral.sh/uv/install.sh | shto install uv.On Windows, run
powershell -ExecutionPolicy ByPass -c “irm https://astral.sh/uv/install.ps1 | iex”to install uv.(Otherwise consult the uv installation docs).
Make sure you have uv installed and available as a command line application. You should be able to open a new shell and run the following command:
$ uv --version
uv 0.9.5 (Homebrew 2025-10-21)By the time you read this, uv will be updated many times, so you should expect to see a newer version.
Create a project with uv
Let’s create a personal Python project for you using uv.
In this example we’ll pretend you’re caled “Alice” and use that as an example. Feel free to sub Alice with your own name or whatever name you want to use for this project.
Let’s open the shell again, and create a project folder. After that let’s change into that folder and run all further commands inside that folder.
mkdir -p alice-python
cd alice-python(On Windows use Powershell, and you can omit the -p flag to mkdir.)
We’ll uv init to initialize the project. Run uv init --help to see the available options.
$ uv init --name alice-python --python 3.13 --description “Alice’s personal Python project”
Initialized project `alice-python`(Again, you can use your own name and description instead of Alice.)
You can inspect the contents of the alice-python directory.
$ ls -A
.git
.gitignore
.python-version
main.py
pyproject.toml
README.mdOpen the folder in your favorite editor, such as Visual Studio Code, PyCharm, or Zed. Review the files that uv init has created for you:
It created a
.gitfolder and added a standard.gitignorefile to help track your project in Git.It created a
pyproject.tomlwhich is the standard (not uv-specific!) config file for Python projects.It created a
main.pywhich is just a dummy file to get you started. We’ll edit this later.It created a
.python-versionfile which tells uv which version of Python to use.It created an empty
README.mdwhich you could also use to take notes during this tutorial.
venv setup
Go back from the editor to the terminal.
Run the following to create and sync the virtual environment (venv):
$ uv sync
Using CPython 3.13.3
Creating virtual environment at: .venv
Resolved 1 package in 17ms
Audited in 0.34msA virtual environment (venv) is a folder (inside the
.venv) that contains all the dependencies of the project. The reason we use them is to isolate the dependencies of this project from the dependencies installed globally on your computer and to isolate it from the dependencies of other projects.
List the files again or take a look at the folder structure in your editor.
$ ls -A
.git
.gitignore
.python-version
.venv
alice
main.py
pyproject.toml
README.md
uv.lockSee that:
A
uv.lockfile has been generated which locks (in other word: pins) all dependencies to a current version.A
.venvfolder has been created and it now contains a valid Python virtual environment. (It’s very similar to a venv you would have created withpython3 -m venv .venv.)
The
uv synccommand does all of the following:* If the requested Python version is not installed on your computer, it will be installed once.
* If the venv (in the.venvfolder) does not exist, it will be created.
* If theuv.lockfile is not present or not up to date, it will be created or updated.
* If the dependencies in the venv are not in sync lockfile, they will be installed.The last part about syncing dependencies is the most important part, this is why the
uv synccommand is calleduv sync. Everything else is a pre-requisite that you could do manually using otheruvsubcommands. (Seeuv python -h,uv venv -h, anduv lock -hrespoectively for details.)Syncing also means that existing dependencies will be upgraded if necessary, and extra dependencies installed in the venv but undeclared in the lockfile will be removed.
venv activation
The way Python venvs work is that you need to activate them in each shell or terminal you open. Do so with the following command:
source .venv/bin/activateRe-run the command if you ever open a new terminal to work on this project. Your terminal should now show the name of the active venv: (alice-python).
You should be able to test your project by running either of the following commands:
$ python main.py
Hello from alice-python!
$
$ python -m main
Hello from alice-python!Better project setup
A quite minimal Python project was created by uv, and we’ll improve it a little bit.
Did you see that the module name is just main? (python -m main?) We’ll rename it to alice, to make it unique. Again, you can use your own name, and you can even check PyPI.org to make sure you’re using a unique name that isn’t taken by anyone else.
Actually there is already a package named
aliceon PyPI. But it’s not related to this tutorial or anything. It’s published by a guy named Walter. Anyways...
Inside the
alice-pythonfolder, create a subfolder namedalice(or your name, but make it lowecase only, keeping it a valid Python module or variable name).Then move
main.pyinside the newalicefolder.
mkdir -p alice
mv main.py alice/Your project setup should look like this:
alice-python/
├── alice/
│ └── main.py
├── pyproject.toml
├── README.md
└── uv.lockThe reason we do this instead of just renaming main.py to alice.py is that we want to prepare for the future where there will be more modules in the alice package.
Edit your pyproject.toml to make the name match the new name you have chosen.
- name = “alice-python”
+ name = “alice”Technically the
namein thepyproject.tomlis the only place where your chosen package name has to be unique and different from other PyPI.org packages if you want to publish it.If you don’t want to publish it, it doesn’t have to be unique in any way.
Let’s assume I want to publish it but there is a package named
alicealready. I could choosename = “alice2025”or anything unique in thepyproject.toml‘snamefield, but keep the folder name simplyalice. This way, I can still publish myalice2025package, but to use it, I can still just doimport alice.(If any other package such as the original
alicepackage or someone else’salice2004package uses thealicefolder name to define analicemodule, then all of these Python packages are mutually incompatible with each other.)
After each change to pyproject.toml, it’s good practice to sync the lockfile and the dependencies again.
$ uv sync
Resolved 1 package in 13ms
Audited in 0.35msAdding dependencies
The last thing we’ll do in this tutorial is to split the code into separate modules, and add a fancy command line interface using Click and Rich.
Run the following to add these dependencies:
uv add click richIt will print something like this, but you might get later versions:
Resolved 7 packages in 22ms
Installed 5 packages in 17ms
+ click==8.3.0
+ markdown-it-py==4.0.0
+ mdurl==0.1.2
+ pygments==2.19.2
+ rich==14.2.0Note that this also added the top-level dependencies in your pyproject.toml:
dependencies = [
“click>=8.3.0”,
“rich>=14.2.0”,
]It also locked the specific version in uv.lock.
You can also manually edit
pyproject.tomlto add, change, upgrade, or remove dependencies. After you make any such changes, just runuv syncagain.
Creating modules
We’ll apply good software engineering princples and separate the core logic from the user interface.
We’ll create the following structure:
alice-python/
├── alice/
│ ├── main.py [already exists]
│ ├── greetings.py [new]
│ └── cli.py [new]
├── pyproject.toml
├── README.md
└── uv.lockOur core business logic is greetings, so let’s create a file alice/greetings.py with the following content:
from rich import print
from rich.text import Text
def hello(name: str):
print(”Hello, “, Text(name, “bold red”), “! :hugging_face:”, sep=”“)
def goodbye(name: str):
print(”Goodbye, “, Text(name, “italic blue”), “! :wave:”, sep=”“)Next, create the command line user interface inside alice/cli.py:
from alice.greetings import hello, goodbye
import click
@click.group()
def cli():
pass
@cli.command(name=”hello”)
@click.option(’--name’, prompt=’Your name’, help=’The person to greet.’)
def hello_cli(name: str):
hello(name)
@cli.command(name=”bye”)
@click.option(’--name’, prompt=’Your name’, help=’The person to say goodbye to.’)
def bye_cli(name: str):
goodbye(name)
if __name__ == “__main__”:
cli()You can try out the CLI by running the following:
$ python -m alice.cli hello --name Alice
Hello, Alice! 🤗
$ python -m alice.cli bye --name Alice
Goodbye, Alice! 👋Thanks to the rich library, you’ll see emojis and colors in the output!
Custom CLI app
Instead of having to run python -m alice.cli, we want to run just a custom CLI named alice. We’ll implement this next.
Edit your pyproject.toml to add the CLI entry point and the required build backend.
[project]
name = “alice-python”
version = “0.1.0”
description = “Alice’s personal Python project”
readme = “README.md”
requires-python = “>=3.13”
dependencies = [
“click>=8.3.0”,
“rich>=14.2.0”,
]
+
+ [project.scripts]
+ alice = “alice.cli:cli”
+
+ [build-system]
+ requires = [”uv_build”]
+ build-backend = “uv_build”
+
+ [tool.uv.build-backend]
+ namespace = true
+ module-name = “alice”
+ module-root = “”The sync the project again to install the new CLI.
uv syncYou can now run the CLI by simply typing alice in the terminal.
$ alice
Usage: alice [OPTIONS] COMMAND [ARGS]...
Options:
--help Show this message and exit.
Commands:
bye
helloThanks to click, it shows the default help message on how to use this CLI. We can use it also in interactive mode, without specifying the name in the command.
$ alice hello
Your name: World
Hello, World! 🤗Conclusion
Congratulations! You’ve created a simple Python project from scratch using uv.
We recommend checking out the uv documentation to see what else you can do with it. Happy coding!




That's a good introduction, thank for this. Wondering though: why didn't you use that `uv init` creates the subdirectory? Are you aware that instead of activating the venv, you could also just use `uv run python -m main.py`. This allows you to almost forget about the venv.