Dev Encyclopedia
ArticlesToolsContactAbout

Get notified when new content drops

No spam. Just new articles, tools, and updates straight to your inbox.

Dev Encyclopedia

A reference for builders

Dev.to
Discord
WhatsApp Channel
daily.dev
Hashnode
X

Content

  • Articles
  • Tools
  • About
  • Contact

Connect

  • support@devencyclopedia.com
  • RSS Feed

Legal

  • Privacy Policy
  • Terms of Service
  • Disclaimer

© 2026 Dev Encyclopedia

Back to top ↑
  1. Home
  2. /Blog
  3. /How to Switch to uv: Replace pip, virtualenv, and Poetry in Your Python Project
python25 min read

How to Switch to uv: Replace pip, virtualenv, and Poetry in Your Python Project

uv replaces pip, virtualenv, and Poetry with a single fast binary. Step-by-step guide to migrating your existing Python project and setting up GitHub Actions CI.

Zeeshan Tofiq
Zeeshan Tofiq
June 4, 2026
On this page

On this page

  • What uv Replaces
  • The Full Command Cheat Sheet
  • Why uv Is This Much Faster
  • Install uv
  • Migrate from pip + requirements.txt
  • Migrate from Poetry
  • Run Your Project
  • Pin Your Python Version
  • GitHub Actions CI
  • Build a Docker Image with uv
  • Migrating from Pipenv
  • How the uv Lockfile Actually Works
  • Replacing pyenv with uv
  • Troubleshooting the Problems You Will Actually Hit
  • No solution found when resolving dependencies
  • Private indexes and authentication
  • Editable installs and local path dependencies
  • uv sync keeps uninstalling a package
  • Hardlink warnings in Docker or on network drives
  • When uv Is Not the Right Call
  • Frequently Asked Questions

Python package management has always been too complicated. To do it properly you needed pip to install packages, virtualenv or venv to isolate environments, pyenv to manage Python versions, and pip-tools to generate a proper lockfile. That's four separate tools, four different config formats, and four things that can break in CI.

uv fixes all of that. It's a single Rust binary from Astral (the same team that built ruff) that handles everything. Not "a little faster" fast: we're talking 10 to 100 times faster than pip on cold installs, and near-instant on warm ones.

This guide is specifically for migrating an existing project. If you're starting fresh, the workflow is even simpler, but most of us have a requirements.txt or pyproject.toml that needs to come along for the ride.

💡 TL;DR

Install uv with curl -LsSf https://astral.sh/uv/install.sh | sh, run uv init in your project root, import your dependencies, then use uv run instead of activating a virtual environment.

What uv Replaces

Old tooluv equivalent
`pip install requests``uv add requests`
`python -m venv .venv``uv venv`
`pyenv install 3.12``uv python install 3.12`
`pip-compile requirements.in``uv lock`
`pip install -r requirements.txt``uv sync`

The Full Command Cheat Sheet

The table above covers the five commands you will type on day one. This one is the version worth keeping open in a browser tab for the first week, because it maps the whole surface area of pip, pip-tools, pyenv, pipx, and Poetry onto uv.

The pattern to internalise is that uv splits every operation into two halves: changing what your project declares (uv add, uv remove, uv lock) and making your environment match that declaration (uv sync, uv run). pip never had that split, which is exactly why pip install could quietly drift your environment away from your requirements file without anyone noticing until a deploy failed.

Common Python packaging tasks mapped from pip, Poetry, and pyenv to uv.
Taskpip + venvPoetryuv
Create an environment`python -m venv .venv``poetry install``uv venv`
Add a dependency`pip install flask``poetry add flask``uv add flask`
Add a dev dependencymanual, second file`poetry add --group dev pytest``uv add --dev pytest`
Remove a dependency`pip uninstall flask``poetry remove flask``uv remove flask`
Install everything`pip install -r requirements.txt``poetry install``uv sync`
Skip dev dependenciessecond requirements file`poetry install --without dev``uv sync --no-dev`
Regenerate the lockfile`pip-compile``poetry lock``uv lock`
Upgrade one package`pip-compile -P django``poetry update django``uv lock --upgrade-package django`
Upgrade everything`pip-compile --upgrade``poetry update``uv lock --upgrade`
Run a command in the envactivate, then run`poetry run pytest``uv run pytest`
Inspect the dependency tree`pipdeptree``poetry show --tree``uv tree`
Install a Python version`pyenv install 3.12`not supported`uv python install 3.12`
Pin the Python version`pyenv local 3.12`not supported`uv python pin 3.12`
Install a global CLI tool`pipx install ruff`not supported`uv tool install ruff`
Run a tool without installingnot supportednot supported`uvx ruff check`
Build a distributable`python -m build``poetry build``uv build`
Publish to PyPI`twine upload dist/*``poetry publish``uv publish`

Two rows deserve a footnote. uvx is not a separate program you have to install: it ships inside the uv binary and is shorthand for uv tool run. It creates a throwaway, cached environment, runs the tool, and leaves nothing behind in your project, which is why it is the right way to run one-off formatters, scaffolders, and migration scripts.

uv tree is the command most people discover last and then use constantly. It prints the resolved dependency graph for your project, and uv tree --invert flips it so you can ask the question you actually care about during an upgrade fight: which of my direct dependencies is dragging in this old version of urllib3?

Why uv Is This Much Faster

It is worth understanding where the speed comes from, because it changes how you use the tool. The gains are not one clever trick, they are four separate design decisions stacked on top of each other.

  • It is a compiled binary, not a Python program. pip has to boot a Python interpreter and import its own dependency tree before it does any work. uv starts in single-digit milliseconds and has no import cost at all.
  • It reads package metadata without downloading packages. To resolve a dependency graph you only need each wheel's metadata file, not the wheel. uv fetches just that slice over HTTP range requests, so resolution touches a fraction of the bytes pip would pull down.
  • Downloads, extraction, and installation happen in parallel. pip processes packages largely one at a time. uv saturates your connection and your CPU cores, which is why the difference is most dramatic on large scientific stacks with dozens of transitive dependencies.
  • There is one global cache, and environments link into it. uv keeps a content-addressed cache (usually ~/.cache/uv) and populates a virtual environment with hardlinks or copy-on-write clones instead of copying files.

That last point has a practical consequence worth changing your habits over. Because a .venv is mostly links into the shared cache, deleting and recreating it is close to free. If an environment ever looks wrong, do not debug it: run rm -rf .venv && uv sync and move on. With pip that was a two minute penalty, so people learned to poke at broken environments instead of rebuilding them.

It also means ten projects that all depend on the same version of numpy store that wheel once on disk, not ten times. The tradeoff is that hardlinks only work when the cache and the virtual environment live on the same filesystem, which is why Docker builds sometimes need UV_LINK_MODE=copy (covered in the troubleshooting section below).

  1. 1

    Install uv

    You don't need Python installed to install uv. It's a standalone binary.

    bash
    curl -LsSf https://astral.sh/uv/install.sh | sh

    On Windows (PowerShell):

    powershell
    powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"

    Restart your terminal and run uv --version to confirm.

  2. 2

    Migrate from pip + requirements.txt

    Navigate to your project root and initialize uv:

    bash
    uv init

    This creates a pyproject.toml if you don't already have one. Then import your existing requirements. If your requirements.txt is clean (no comments, no extras), you can do it in one line:

    bash — Quick import
    uv add $(cat requirements.txt | grep -v "^#" | xargs)

    Or use uv's pip-compatible interface for a file with pinned versions, comments, or extras:

    bash — Safe alternative
    uv pip install -r requirements.txt

    After that, run uv lock to generate your lockfile. Commit both pyproject.toml and uv.lock. Going forward, use uv add package-name instead of pip install, and uv run python script.py instead of activating the virtual environment manually.

    💡 Tip

    The lockfile uv.lock should be committed to your repo. This is the opposite of what some tools recommend for lockfiles: uv's lockfile is what guarantees every developer and every CI run installs the exact same versions.

  3. 3

    Migrate from Poetry

    The easiest path is a tool called migrate-to-uv, which runs via uvx (uv's tool runner, like npx for Python):

    bash
    uvx migrate-to-uv

    Run it in your project root. It reads your pyproject.toml (Poetry format) and converts it to the uv format. Then rebuild:

    bash
    uv lock && uv sync

    A few things that don't auto-migrate cleanly:

    • Optional dependency groups: Poetry uses [tool.poetry.group.dev.dependencies]. uv uses [dependency-groups] with slightly different syntax. Check these manually after migration.
    • Private indexes: If your Poetry config references a private PyPI index, set it up in [tool.uv.sources] in your new pyproject.toml.
    • poetry run calls: Replace all poetry run X with uv run X across your scripts and documentation.
  4. 4

    Run Your Project

    Once uv is managing your project, use uv run for everything. You don't need to activate the virtual environment manually:

    bash
    # Instead of: source .venv/bin/activate && python main.py
    uv run python main.py
    
    # Instead of: source .venv/bin/activate && pytest
    uv run pytest
    
    # Instead of: source .venv/bin/activate && flask run
    uv run flask run

    ℹ Info

    If you prefer activating the environment, the .venv folder is still there at your project root. source .venv/bin/activate works exactly as before. uv run is just more convenient for one-off commands.

  5. 5

    Pin Your Python Version

    To pin a specific Python version for your project, create a .python-version file in your project root. uv reads this file automatically:

    bash
    echo "3.12" > .python-version

    You can also install that Python version via uv if it's not already on your system:

    bash
    uv python install 3.12

    💡 Tip

    Commit .python-version to your repo. This ensures everyone on the team and all CI runs use the same Python version without needing pyenv or any other version manager.

  6. 6

    GitHub Actions CI

    This is where uv's speed pays off most visibly. CI recreates your environment from scratch on every run: the difference between pip and uv is often 2 to 3 minutes vs 15 to 20 seconds.

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

    ⚠ Warning

    The --frozen flag is critical. It tells uv to use uv.lock exactly as committed and fail the build if the lockfile is out of date. Without it, CI could silently install different versions than your lockfile specifies.

    One addition worth making to that workflow: astral-sh/setup-uv can cache the uv download cache between runs. Turn it on with enable-cache: true and CI installs drop from seconds to fractions of a second on cache hits.

    yaml — .github/workflows/ci.yml (cached)
          - uses: astral-sh/setup-uv@v4
            with:
              enable-cache: true
              cache-dependency-glob: "uv.lock"

    The cache-dependency-glob tells the action to invalidate the cache only when uv.lock changes, which is exactly the right granularity. If you are building out a pipeline from scratch, the GitHub Actions tutorial covers the workflow syntax around this step in more detail.

  7. 7

    Build a Docker Image with uv

    Docker is the other place uv pays for itself immediately, but only if you structure the Dockerfile so dependency installation is its own cacheable layer. The mistake almost everyone makes first is copying the whole project in before installing, which busts the dependency layer on every single source change.

    The pattern below installs dependencies from pyproject.toml and uv.lock alone, then copies the application code afterwards. Edit a Python file and only the last two layers rebuild.

    dockerfile — Dockerfile
    FROM python:3.12-slim-bookworm
    
    # Copy the uv binary straight out of the official image, pinned by tag
    COPY --from=ghcr.io/astral-sh/uv:0.9 /uv /uvx /bin/
    
    WORKDIR /app
    
    ENV UV_COMPILE_BYTECODE=1 \
        UV_LINK_MODE=copy
    
    # Layer 1: dependencies only. Cached until uv.lock or pyproject.toml changes.
    RUN --mount=type=cache,target=/root/.cache/uv \
        --mount=type=bind,source=uv.lock,target=uv.lock \
        --mount=type=bind,source=pyproject.toml,target=pyproject.toml \
        uv sync --locked --no-install-project --no-dev
    
    # Layer 2: your code, which changes on every commit
    COPY . /app
    RUN --mount=type=cache,target=/root/.cache/uv \
        uv sync --locked --no-dev
    
    ENV PATH="/app/.venv/bin:$PATH"
    
    CMD ["uv", "run", "--no-sync", "gunicorn", "app.wsgi:application"]

    Four details in that file carry most of the benefit. UV_COMPILE_BYTECODE=1 precompiles .pyc files during the build so your container does not pay that cost on first request, which matters for cold starts. UV_LINK_MODE=copy disables hardlinking, because the uv cache mount and the container filesystem are different mounts and hardlinks across them are impossible.

    --no-install-project in the first layer installs your dependencies but not your own package, which is what keeps that layer stable across code changes. And the --mount=type=cache line persists uv's download cache across builds without baking it into the image.

    ℹ Info

    Pin the uv image to a minor version (uv:0.9) rather than latest. A floating tag means your builds can change behaviour on a day you did not touch anything, which is the exact class of problem lockfiles exist to prevent.

    For a smaller final image, use a multi-stage build: run the uv sync steps in a builder stage, then copy only /app/.venv into a fresh python:3.12-slim stage. uv itself does not need to exist in the runtime image at all, as long as you put the virtual environment's bin directory on PATH. If you run these images under Podman rather than Docker, the Compose to Quadlet migration guide covers how the run-time side of that changes.

Migrating from Pipenv

Pipenv projects migrate more cleanly than most people expect, because Pipfile and Pipfile.lock carry the same information uv needs. The same migrate-to-uv tool used for Poetry detects a Pipfile automatically:

bash
uvx migrate-to-uv

If you would rather do it by hand (worth it when your Pipfile has unusual markers or VCS dependencies), Pipenv can print your dependencies in requirements format and uv can read that file directly:

bash
pipenv requirements > requirements.txt
pipenv requirements --dev-only > requirements-dev.txt

uv init
uv add -r requirements.txt
uv add --dev -r requirements-dev.txt

uv lock
uv sync

Once uv.lock is generated and your tests pass, delete Pipfile, Pipfile.lock, and the two temporary requirements files. Keeping them around invites someone to run pipenv install six months from now and quietly build a second environment.

The one feature with no direct replacement is Pipenv's [scripts] section. uv is a package manager, not a task runner, and it does not read a scripts table from pyproject.toml. You have three reasonable options.

  1. Declare real console entry points under [project.scripts] in pyproject.toml. These become executables inside the environment and run with uv run my-command. This is the right choice for anything that is genuinely part of your package.
  2. Keep a Makefile or a justfile where each target wraps a uv run ... command. This is the most common pattern in uv codebases and it keeps task definitions out of your package metadata.
  3. Use a dedicated task runner such as poethepoet, installed as a dev dependency, if you want the tasks to live in pyproject.toml.

If you are coming from the JavaScript side and missing the ergonomics of a scripts block, the patterns in npm scripts you should know translate almost directly onto a Makefile wrapping uv run.

How the uv Lockfile Actually Works

uv.lock is not a pip freeze dump with a different name, and the difference matters. A frozen requirements file records what happened to be installed on one machine, running one Python version, on one operating system. Regenerate it on a colleague's laptop and you get a different file.

uv.lock is a universal lockfile. uv resolves your dependency graph for every platform and every Python version allowed by your requires-python range, then records all of those resolutions in one file with environment markers attached. A Linux CI runner and an Apple Silicon laptop read the same lockfile and each install the correct wheels from it.

toml — uv.lock (excerpt)
version = 1
requires-python = ">=3.11"

[[package]]
name = "requests"
version = "2.32.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
    { name = "certifi" },
    { name = "charset-normalizer" },
    { name = "idna" },
    { name = "urllib3" },
]
sdist = { url = "https://files.pythonhosted.org/.../requests-2.32.3.tar.gz", hash = "sha256:55365...", size = 131218 }
wheels = [
    { url = "https://files.pythonhosted.org/.../requests-2.32.3-py3-none-any.whl", hash = "sha256:70761...", size = 64928 },
]

Every entry carries a hash, so an install is verified against the exact artifact that was resolved. If a package is ever yanked and republished with different contents, your install fails loudly instead of silently picking up different code.

The file is generated, so never hand-edit it. Change pyproject.toml, then re-lock. Note that uv add and uv remove update pyproject.toml, uv.lock, and your environment in a single command, so you rarely call uv lock directly outside of upgrades.

Which uv command to reach for depending on how strict you need to be.
CommandWhat it doesUse it when
`uv sync`Re-locks if `pyproject.toml` changed, then installsEveryday local development
`uv sync --locked`Fails if `uv.lock` is out of date with `pyproject.toml`CI, where a stale lockfile should break the build
`uv sync --frozen`Installs `uv.lock` as-is, never re-resolvesDocker builds and deploys
`uv sync --no-dev`Installs runtime dependencies onlyProduction images and release artifacts
`uv lock --check`Verifies the lockfile is current without installingA fast pre-commit or CI lint step
`uv lock --upgrade-package X`Re-resolves one package, leaves the rest pinnedTargeted security patches

The distinction between --locked and --frozen trips people up, so it is worth stating plainly. --locked verifies that the lockfile still matches pyproject.toml and errors if it does not. --frozen skips that check entirely and installs whatever the lockfile says. Use --locked in CI, where you want a forgotten uv lock to fail the pull request, and --frozen in a Docker build, where you may not even copy pyproject.toml into the layer.

If something outside your project still expects a requirements.txt (an old deployment script, a security scanner, a platform that only understands pip), uv can generate one from the lockfile without giving up the lockfile as the source of truth:

bash
uv export --no-dev --no-hashes -o requirements.txt

⚠ Warning

Treat an exported requirements.txt as a build artifact, not a source file. Add it to .gitignore or regenerate it in CI. The moment two people can edit both it and pyproject.toml, you are back to the drift problem uv was supposed to end.

Replacing pyenv with uv

pyenv compiles CPython from source. That means you need a C compiler, the right development headers for OpenSSL, readline, zlib, and libffi, and roughly two to five minutes per version. When a build fails, the error is usually a missing system library, three layers deep in a make log.

uv skips all of that. It downloads prebuilt standalone CPython distributions, so installing an interpreter is a download and an extract, typically a few seconds. Nothing is compiled, nothing touches your system Python, and the interpreters live in uv's own directory rather than being wired into your shell through a shim.

bash
# See what is available and what you already have
uv python list

# Install specific versions
uv python install 3.11 3.12 3.13

# Pin this project (writes .python-version)
uv python pin 3.12

# Run a one-off command against a different interpreter
uv run --python 3.11 pytest

There are two version declarations in a uv project and they answer different questions. .python-version says which interpreter this working copy should use right now. requires-python in pyproject.toml says which Python versions your code supports, and it constrains the resolver: set requires-python = ">=3.11" and uv will refuse to lock a dependency that dropped 3.11 support.

Libraries should set a range in requires-python and test the edges. Applications can be stricter, because you control the deployment target. Either way, commit .python-version so nobody on the team is silently running a different interpreter, and so uv python install with no arguments in CI knows what to fetch.

pyenvuv
`pyenv install 3.12.7``uv python install 3.12.7`
`pyenv versions``uv python list --only-installed`
`pyenv local 3.12``uv python pin 3.12`
`pyenv global 3.12`no equivalent (uv is per-project by design)
`pyenv uninstall 3.11``uv python uninstall 3.11`

ℹ Info

You can run uv alongside pyenv during a transition. uv will happily use an interpreter that pyenv installed if it satisfies your requires-python. Once every project has a .python-version and a lockfile, remove the pyenv shims from your shell profile.

Troubleshooting the Problems You Will Actually Hit

Most uv migrations are uneventful. These are the five failures that account for nearly all of the ones that are not.

No solution found when resolving dependencies

This is the most common shock for teams coming from pip, and it is not a uv bug. pip installs sequentially and lets a later package overwrite an earlier one's dependency, so a genuinely impossible requirement set can appear to install fine and then break at runtime. uv resolves the whole graph up front, so it tells you at install time.

uv's error output names the specific conflicting requirements and the packages that introduced them. Read it before changing anything: it usually points straight at one over-tight pin. Then work through the options in order:

  1. Run uv tree --invert --package urllib3 (substituting the conflicting package) to see which of your direct dependencies is imposing the constraint.
  2. Loosen your own pins first. A dependency pinned with == in your pyproject.toml that could be >= is the cause more often than a genuine upstream conflict.
  3. If a transitive dependency is the problem and you cannot wait for an upstream release, add an override. [tool.uv] override-dependencies = ["urllib3>=2"] forces a version across the whole graph, ignoring what other packages requested.
  4. Use constraint-dependencies instead when you want to narrow a version range without forcing a package to be installed at all.

⚠ Warning

Overrides are a loaded gun: you are telling the resolver that a package's stated requirements are wrong. Leave a comment explaining why each override exists and which upstream issue removes the need for it, or it will still be there in three years.

Private indexes and authentication

Internal package servers (Artifactory, Nexus, CodeArtifact, a private PyPI mirror) are configured in pyproject.toml rather than in a global pip config, which means the configuration is version controlled and every developer gets it automatically.

toml — pyproject.toml
[[tool.uv.index]]
name = "internal"
url = "https://pypi.internal.example.com/simple"
explicit = true

[tool.uv.sources]
acme-billing = { index = "internal" }

Setting explicit = true is the security-conscious choice. It means uv only looks at that index for packages explicitly routed to it under [tool.uv.sources], and everything else comes from PyPI. Without it, an index that serves an unexpected package name can shadow a public one, which is the shape of a dependency confusion attack.

Keep credentials out of the URL. uv reads them from environment variables named after the index, so an index named internal uses UV_INDEX_INTERNAL_USERNAME and UV_INDEX_INTERNAL_PASSWORD. In CI those come from your secrets store, and locally they can live in your shell profile or a credential helper.

Editable installs and local path dependencies

pip install -e . has a direct equivalent, and in a uv project you usually do not need it at all: uv sync installs your own package in editable mode by default. Where it matters is a sibling library you are developing alongside the application.

bash
uv add --editable ../shared-lib

That records a path source in pyproject.toml, so imports resolve to your working copy and edits take effect without reinstalling. The catch is that a path source is machine-specific: it will not work for anyone who does not have that directory checked out at the same relative location.

For anything more than a quick experiment, convert the setup into a workspace instead. Workspace members are declared once at the repository root, share a single lockfile, and work identically for every developer and in CI.

toml — pyproject.toml (workspace root)
[tool.uv.workspace]
members = ["packages/*"]

[tool.uv.sources]
shared-lib = { workspace = true }

uv sync keeps uninstalling a package

If you install something with uv pip install and then run uv sync, it disappears. That is correct behaviour, not a bug. uv sync makes the environment match the lockfile exactly, and anything not declared in pyproject.toml is by definition not in the lockfile.

The fix is to stop mixing interfaces. uv pip install exists as a compatibility shim for scripts and workflows that have not migrated yet; in a project with a uv.lock, use uv add. If you only want a package for a single command and not permanently, use the --with flag instead of installing anything:

bash
uv run --with ipython ipython

Hardlink warnings in Docker or on network drives

A warning about failing to hardlink files means the uv cache and the target environment are on different filesystems. This happens inside Docker with a cache mount, on some CI runners, and when a project lives on a network share or a mounted volume.

It is a performance warning rather than an error, and the fix is one environment variable: UV_LINK_MODE=copy. Set it in your Dockerfile or CI environment. You can also relocate the cache next to the environment with UV_CACHE_DIR so hardlinking works again, which is usually the faster option on a build machine you control.

When uv Is Not the Right Call

Honest limitations, because every migration guide that claims none is hiding something.

  • You depend on conda for non-Python packages. uv installs from PyPI. If your stack needs CUDA toolkits, compiled geospatial libraries, or an R runtime managed by conda, uv does not replace that. Most pure-Python and wheel-distributed scientific work is fine.
  • Your organisation only mirrors packages in a format uv cannot reach. Check that your internal index speaks the standard PyPI simple API before committing to the migration.
  • You need a task runner in `pyproject.toml`. uv does not have one. This is a deliberate scope decision, not an oversight, and a Makefile covers it.
  • Your team cannot absorb a resolver that is stricter than pip. If your dependency graph has real conflicts that pip has been papering over, the migration surfaces them all at once. That is a good outcome, but budget time for it rather than doing it the day before a release.

For everything else, the migration is genuinely a couple of hours for a typical service and the payback is immediate: faster CI, faster Docker builds, one tool instead of four, and a lockfile that means the same thing on every machine. Once uv is in place, the rest of your Python tooling gets easier to modernise too, whether that is adopting ruff or moving onto newer language features like the ones in t-strings.

Frequently Asked Questions

How much faster is uv than pip?

In benchmarks, uv installs packages 10 to 100 times faster than pip on cold cache and near-instant on warm cache. The difference is most noticeable in CI where the cache is cold on every run.

Scenariopipuv
Install Django cold~45 seconds~3 seconds
Install Django warm~8 seconds<1 second
Full data science stack cold~4 minutes~15 seconds
Should I commit uv.lock to my repository?

Yes, always commit uv.lock. It records the exact resolved versions of every dependency (including transitive ones). Without it, two developers running uv sync at different times might install different patch versions of a dependency, leading to 'works on my machine' bugs.

💡 Tip

The only time you'd exclude uv.lock from a repo is for a library package (not an application) that intentionally supports a range of dependency versions. For applications and services, always commit the lockfile.

Does uv support monorepos and workspaces?

Yes. uv supports workspaces similar to Cargo (Rust) and npm workspaces. Define workspace members in your root pyproject.toml:

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

Each member has its own pyproject.toml, but they share a single uv.lock at the workspace root. Dependencies are resolved together, preventing version conflicts across packages.

How do I add a dev-only dependency with uv?

Use the --dev flag:

bash
uv add --dev pytest ruff mypy

This adds the package to the [dependency-groups] dev section of pyproject.toml rather than the main [project.dependencies]. Dev dependencies are installed with uv sync (default) but excluded when you do uv sync --no-dev for production deploys.

Can uv replace pipx for globally installed tools?

Yes. Use uv tool install to install CLI tools globally:

bash
uv tool install ruff
uv tool install black
uv tool install httpie

Installed tools are isolated from each other and from your projects. Use uvx tool-name to run a tool once without installing it permanently: the equivalent of npx in the JavaScript ecosystem.

Can I use uv and pip in the same project during migration?

You can, and for a transition period it is the pragmatic choice. uv pip install, uv pip compile, and uv pip sync mirror the pip and pip-tools interfaces closely enough that most existing scripts work by prefixing them with uv. That alone gets you the speed win with almost no risk.

What you should not do is mix uv pip install with uv sync in a project that already has a uv.lock. The two interfaces have different ideas about what owns the environment: uv sync will remove anything that is not in the lockfile. Pick one per project and finish the migration rather than living in both.

How do I generate a requirements.txt from uv?

Use uv export, which reads the lockfile rather than the installed environment, so the output is reproducible:

bash
# Runtime dependencies only, no hashes
uv export --no-dev --no-hashes -o requirements.txt

Drop --no-hashes if the consumer supports hash verification, which you generally want for anything that installs into production. Regenerate the file in CI instead of committing it, so it can never disagree with uv.lock.

Does uv replace conda for data science work?

Partly. uv installs from PyPI, so it handles anything distributed as a wheel, which now includes numpy, pandas, scikit-learn, PyTorch, and most of the stack that historically forced people onto conda. For those projects uv is faster and simpler.

It does not replace conda when you need conda to manage non-Python system packages: CUDA toolkits, GDAL and its C dependencies, MKL builds, or an environment that mixes Python with R. conda is a general-purpose binary package manager and uv is deliberately not trying to be one.

How do I upgrade a single dependency without touching the rest?

Re-lock just that package, then sync:

bash
uv lock --upgrade-package django
uv sync

uv re-resolves django and anything it strictly requires, and leaves every other pin in the lockfile untouched. This is what you want for a security patch: the resulting diff is small enough to review, and the blast radius of the change is visible in the pull request.

uv lock --upgrade with no package name upgrades everything within the constraints in pyproject.toml. Save that for a deliberate maintenance window with the test suite in front of you, not for a Friday afternoon.

Zeeshan Tofiq

Zeeshan Tofiq

Full Stack Developer

Full stack developer with over 6 years of experience building production applications. Writes practical guides on JavaScript, TypeScript, React, Node.js, and cloud infrastructure. Focused on helping developers solve real problems with clean, maintainable code.

Enjoyed this article?

Get practical dev guides, tool updates, and new articles delivered straight to your inbox. No spam, unsubscribe anytime.

Related Articles

nextjs

Husky + Prettier + lint-staged Setup for Next.js

Set up Husky v9, Prettier, and lint-staged in your Next.js project. Step-by-step guide covering pre-commit hooks with the correct 2026 config.

May 30, 2026·20 min read
databases

Drizzle ORM Migrations: A Practical drizzle-kit Guide

Learn the full Drizzle ORM migration workflow: push vs migrate, drizzle-kit setup, Turso/libSQL config, team conflicts, and production best practices.

May 30, 2026·20 min read

On this page

  • What uv Replaces
  • The Full Command Cheat Sheet
  • Why uv Is This Much Faster
  • Install uv
  • Migrate from pip + requirements.txt
  • Migrate from Poetry
  • Run Your Project
  • Pin Your Python Version
  • GitHub Actions CI
  • Build a Docker Image with uv
  • Migrating from Pipenv
  • How the uv Lockfile Actually Works
  • Replacing pyenv with uv
  • Troubleshooting the Problems You Will Actually Hit
  • No solution found when resolving dependencies
  • Private indexes and authentication
  • Editable installs and local path dependencies
  • uv sync keeps uninstalling a package
  • Hardlink warnings in Docker or on network drives
  • When uv Is Not the Right Call
  • Frequently Asked Questions