Skip to content

How to Create a New Python Project with uv init

I tried setting up a new Python project last week. I created a virtual environment, installed dependencies, set up my pyproject.toml, configured linting tools, and more. Two hours later, I still hadn’t written any actual code.

The traditional Python project setup is slow. You need to choose between pip, poetry, or pipenv. You need to configure virtual environments, dependency files, and build systems. You need to install and configure linting and formatting tools.

Then I discovered uv init. It creates a complete Python project in seconds.

Why I Switched to uv for Project Setup

I used to spend the first day of any new project on setup. I would:

  1. Create a virtual environment with python -m venv .venv
  2. Activate it (different commands on Windows vs Unix)
  3. Install dependencies with pip
  4. Create a requirements.txt or pyproject.toml
  5. Set up black, flake8, and isort
  6. Configure pre-commit hooks
  7. Create a .gitignore file

Each step required reading documentation or copying from previous projects.

uv solves this by doing everything in one command. It creates the project structure, manages dependencies, handles virtual environments, and even manages Python versions. The speed difference is noticeable—uv installs packages 10-100x faster than pip.

Creating Your First Project

Let me create a simple application project:

create-application.sh
uv init my-app
cd my-app

This creates a complete project structure:

my-app/
├── .gitignore
├── .python-version
├── README.md
├── main.py
└── pyproject.toml

The .python-version file pins the Python version. The pyproject.toml contains project metadata and dependencies. The main.py is your entry point with a simple “Hello, world!” example.

Let me check what uv init created:

check-project.sh
cat pyproject.toml

Output:

pyproject.toml
[project]
name = "my-app"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
dependencies = []

The project is ready to use. No manual configuration needed.

Choosing the Right Project Type

uv init supports three project types:

Application projects (--app) are for web servers, CLIs, and scripts. This is the default:

create-app.sh
uv init --app my-web-server

Library projects (--lib) are for packages you plan to publish:

create-lib.sh
uv init --lib my-library

Library projects include a src/ directory structure:

my-library/
├── .gitignore
├── .python-version
├── README.md
├── pyproject.toml
├── src/
│ └── my_library/
│ └── __init__.py
└── tests/
└── __init__.py

Script projects (--script) create single-file scripts with inline metadata:

create-script.sh
uv init --script my-script.py

This creates a standalone script with embedded dependency declarations.

Adding Dependencies

Now I need to add some packages to my project:

add-dependencies.sh
cd my-app
uv add requests flask

Output:

Resolved 12 packages in 0.45s
Installed 12 packages in 0.12s
+ flask-3.0.0
+ requests-2.31.0
...

uv creates a uv.lock file to ensure reproducible installs. It also creates a .venv directory automatically.

Let me add development dependencies for testing and linting:

add-dev-dependencies.sh
uv add --dev pytest ruff

My pyproject.toml now includes:

updated-pyproject.toml
[project]
name = "my-app"
version = "0.1.0"
dependencies = [
"flask>=3.0.0",
"requests>=2.31.0",
]
[tool.uv]
dev-dependencies = [
"pytest>=8.0.0",
"ruff>=0.2.0",
]

The lock file ensures everyone on the team gets the exact same versions.

Running Your Code

I used to activate virtual environments manually:

old-way.sh
source .venv/bin/activate # Unix
# or
.venv\Scripts\activate # Windows
python main.py

With uv, I just run:

run-with-uv.sh
uv run main.py

uv automatically uses the project’s virtual environment. I don’t need to remember activation commands or worry about which Python I’m using.

For scripts with dependencies, uv run is even more useful. It installs dependencies on first run:

run-script.sh
uv run my-script.py

This works for any command, not just Python scripts:

run-tools.sh
uv run pytest
uv run ruff check .
uv run flask run

Working with Multiple Python Versions

I needed to test my code on Python 3.11 and 3.12. With uv, I don’t install Python manually:

set-python-version.sh
uv python install 3.11 3.12
uv python pin 3.11

uv downloads and manages Python installations automatically. The .python-version file tells uv which version to use.

To test on multiple versions:

test-multi-version.sh
uv python pin 3.11
uv run pytest
uv python pin 3.12
uv run pytest

No pyenv or manual installation needed.

Setting Up Workspaces

I had a project with multiple packages. I used separate repositories or a complex poetry setup. uv makes this simpler with workspaces:

create-workspace.sh
mkdir my-workspace
cd my-workspace
uv init
# Create workspace members
uv init packages/core
uv init packages/api
uv init packages/web

The root pyproject.toml defines the workspace:

workspace-pyproject.toml
[tool.uv.workspace]
members = ["packages/*"]

Each package can depend on others:

add-workspace-dep.sh
cd packages/api
uv add core # References packages/core

Dependencies within the workspace use local paths, not published packages. This is perfect for monorepos.

Building and Publishing Libraries

For library projects, I wanted to build and publish to PyPI. uv handles this too:

build-and-publish.sh
cd my-library
# Build distributions
uv build
# Creates:
# dist/my_library-0.1.0-py3-none-any.whl
# dist/my_library-0.1.0.tar.gz
# Publish to PyPI
uv publish

uv prompts for PyPI credentials or uses tokens from the environment. No need for twine or build tools.

Integrating with CI/CD

I set up GitHub Actions for my project. The workflow is simple because uv handles everything:

.github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v4
- name: Set up Python
run: uv python install 3.12
- name: Install dependencies
run: uv sync
- name: Run tests
run: uv run pytest
- name: Lint
run: uv run ruff check .

The uv sync command creates the virtual environment and installs all dependencies from the lock file. This ensures CI matches local development.

Comparing uv with Other Tools

I used poetry before switching to uv. Here’s what I noticed:

Speed: uv is significantly faster. Installing Django and its dependencies took 8 seconds with uv vs 45 seconds with poetry.

Simplicity: uv has fewer commands to learn. uv init, uv add, uv run cover most needs. Poetry has separate commands for adding, updating, and installing.

Tool integration: uv includes ruff for linting and formatting. I don’t need to configure black, flake8, and isort separately.

Python management: uv installs and manages Python versions. Poetry requires you to install Python separately.

The traditional pip approach is even slower. Creating a venv, activating it, pip installing, and managing requirements.txt files takes more steps. pip also doesn’t have a lock file by default, making reproducible builds harder.

Common Mistakes I Made

When I started with uv, I made some mistakes:

Not using uv run: I tried activating the virtual environment manually. This defeats uv’s purpose. Use uv run for all commands.

Using pip inside uv projects: I ran pip install in a uv-managed project. This broke the lock file consistency. Always use uv add instead.

Ignoring the lock file: I didn’t commit uv.lock to git initially. This caused different dependency versions across machines. Now I always commit it.

Forgetting .python-version: I didn’t pin the Python version in some projects. Team members had different Python versions installed. uv makes this easy—just commit the .python-version file.

Practical Workflow

Here’s my typical workflow for a new project:

complete-workflow.sh
# Create the project
uv init my-project
cd my-project
# Add production dependencies
uv add flask sqlalchemy
# Add dev dependencies
uv add --dev pytest ruff basedpyright
# Create a simple test
cat > test_main.py << 'EOF'
def test_example():
assert True
EOF
# Run tests
uv run pytest
# Check code
uv run ruff check .
# Type check
uv run basedpyright
# Run the application
uv run main.py

In about two minutes, I have a fully configured project with dependencies, tests, and linting.

When to Use Each Project Type

I learned to choose based on the project’s purpose:

  • Application (--app): Use for web servers, CLIs, scripts, and most projects. This is the sensible default.
  • Library (--lib): Use only when you plan to publish to PyPI. The src/ layout helps with packaging.
  • Script (--script): Use for single-file utilities. Inline metadata keeps everything in one file.

Most of my projects are applications, so I rarely need --lib or --script.

Summary

In this post, I showed how to create Python projects with uv. The uv init command creates a complete project structure with pyproject.toml, virtual environments, and Python version management. I covered the three project types (application, library, and script), adding dependencies with uv add, running code with uv run, and setting up workspaces for monorepos. I also shared my workflow and common mistakes to avoid.

The key benefit is speed—uv handles project setup in seconds instead of hours. You get dependency management, virtual environments, Python version management, and build tools in one package. No more juggling pip, poetry, pyenv, and build tools separately.

Final Words + More Resources

My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me

Here are also the most important links from this article along with some further resources that will help you in this scope:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments