# uv (Python package manager)

> uv is Astral's Rust-based Python package and project manager that resolves dependencies into a hashed, cross-platform uv.lock and installs from it.

Also known as: uv python, Astral uv, uvx, uv package manager, uv.lock

Source: https://socket.dev/glossary/uv

Last updated: 2026-10-15

uv is a Python package and project manager from Astral, written in Rust, that replaces pip, pip-tools, virtualenv, pipx, and pyenv. A uv Python project resolves once into a cross-platform, hash-pinned uv.lock file and installs from it. That makes installs fast and reproducible, but locking, source builds, and uvx can still run untrusted code.

We tested uv 0.13.1, the current release as of October 15, 2026. Its installs are tightly controlled: one altered hash in uv.lock stopped `uv sync` cold, and its default index strategy blocks the classic dependency confusion attack. The exposure sits elsewhere. `uv lock` executed a dependency's setup.py before anything was installed. A hand-injected lockfile entry passed `uv lock --check` and installed. A cooldown passed on the command line vanished on the next `uv sync`, and `uvx` resolves tools against the live index with no lockfile at all.

Our position: uv's defaults are safer than pip's and Poetry's, and the commands that resolve dependencies, not the ones that install them, are where a uv project's supply chain is open. Treat `uv add`, `uv lock`, and `uvx` like running a stranger's code, because sometimes they do.

## What is uv in Python?

uv is a single static binary that manages a Python project end to end: it installs Python itself, creates the virtual environment, resolves dependencies into a lockfile, installs them, runs commands, builds distributions, and publishes them. Astral's documentation describes it as "a single tool to replace pip, pip-tools, pipx, poetry, pyenv, twine, virtualenv, and more" ([uv docs](https://docs.astral.sh/uv/)).

uv first appeared on PyPI on February 15, 2024 ([release history](https://pypi.org/project/uv/#history)). It began as a faster pip and grew into a project manager in August 2024, when Astral added lockfiles, workspaces, tool management, and Python installation ([our coverage](https://socket.dev/blog/uv-python-s-new-high-speed-package-manager)). It is still versioned 0.x: 0.13.1 shipped on October 14, 2026.

Adoption is now mainstream. PyPI recorded 127,754,131 downloads of the uv package in the 30 days to October 15, 2026, against 624,210,278 for pip ([pypistats](https://pypistats.org/packages/uv)). That understates uv, because its curl installer and GitHub release binaries bypass PyPI. On March 19, 2026, Astral announced an agreement to join OpenAI's Codex team and said "OpenAI will continue supporting our open source tools after the deal closes" ([Astral](https://astral.sh/blog/openai)).

The everyday workflow is four commands. This is a real session:

```bash
$ uv --version
uv 0.13.1 (x86_64-unknown-linux-gnu)
$ uv init proj && cd proj
Initialized project `proj` at `/work/proj`
$ uv add requests
Using CPython 3.13.16 interpreter at: /usr/bin/python3.13
Creating virtual environment at: .venv
Resolved 6 packages in 4ms
Installed 6 packages in 3ms
 + certifi==2026.7.22
 + charset-normalizer==3.5.2
 + idna==3.20
 + proj==0.1.0 (from file:///work/proj)
 + requests==2.34.2
 + urllib3==2.8.0
$ uv tree
proj v0.1.0
└── requests v2.34.2
    ├── certifi v2026.7.22
    ├── charset-normalizer v3.5.2
    ├── idna v3.20
    └── urllib3 v2.8.0
```

`uv add` edited pyproject.toml, resolved the tree, wrote uv.lock, created `.venv`, and installed. `uv run <cmd>` locks, syncs, and runs a command inside that environment, so nobody has to activate a virtual environment by hand. uv also keeps a pip-compatible interface, `uv pip install`, which works like [pip](https://socket.dev/glossary/pip-python-package-index) and does not use uv.lock.

## Why is uv faster than pip and Poetry?

uv is faster than pip and [Poetry](https://socket.dev/glossary/poetry) mainly because of the work it avoids, not because it is written in Rust. Andrew Nesbitt's analysis concludes that "uv is fast because of what it doesn't do, not because of what language it's written in" ([How uv got so fast](https://nesbitt.io/2025/12/26/how-uv-got-so-fast.html)). Astral's own claim is 10 to 100 times faster than pip.

The biggest saving is that uv rarely downloads a package to learn its dependencies. [PyPI](https://socket.dev/glossary/python-package-index-pypi) serves each file's core metadata separately (PEP 658), so the resolver reads a few hundred bytes instead of a full wheel. This is the record PyPI's Simple API returns for the requests wheel above:

```bash
$ curl -s -H "Accept: application/vnd.pypi.simple.v1+json" \
    https://pypi.org/simple/requests/ | jq '.files[] | select(.filename=="requests-2.34.2-py3-none-any.whl")'
{
  "core-metadata": {
    "sha256": "8c384ba3e979480faae2859d3c5e6c1276dd2c3616e322e124d52c8cfc556f27"
  },
  "filename": "requests-2.34.2-py3-none-any.whl",
  "hashes": {
    "sha256": "2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0"
  },
  "provenance": "https://pypi.org/integrity/requests/2.34.2/requests-2.34.2-py3-none-any.whl/provenance",
  "requires-python": ">=3.10",
  "size": 73075,
  "upload-time": "2026-05-14T19:25:26.443000Z",
  "url": "https://files.pythonhosted.org/packages/a0/f4/.../requests-2.34.2-py3-none-any.whl",
  "yanked": false
}
```

When no metadata file exists, uv reads the zip central directory at the end of a wheel with HTTP range requests, and only then downloads the whole file. The remaining savings are architectural. Downloads run in parallel. A global cache holds one copy of each unpacked package and hardlinks or clones it into every environment: `requests/sessions.py` in our project had a link count of 3, the cache plus two virtual environments. uv skips bytecode compilation by default, and its PubGrub resolver learns from each conflict instead of retrying similar dead ends.

Every field in that record matters for security too. `hashes` is what uv.lock pins. `upload-time` is what dependency cooldowns filter on. `provenance` points to a [PEP 740 attestation](https://socket.dev/blog/pypi-introduces-digital-attestations) that, as of 0.13.1, uv uploads when publishing but exposes no option to verify when installing.

## How does a uv Python project lock its dependencies?

A uv Python project locks its dependencies with one universal resolution: uv solves for every platform and Python version the project supports and writes the result to uv.lock. One file serves a macOS laptop and a Linux CI runner alike ([uv docs](https://docs.astral.sh/uv/concepts/resolution/)). This is the entry for requests:

```text
[[package]]
name = "requests"
version = "2.34.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
    { name = "certifi" },
    { name = "charset-normalizer" },
    { name = "idna" },
    { name = "urllib3" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/.../requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" }
wheels = [
    { url = "https://files.pythonhosted.org/packages/a0/f4/.../requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" },
]
```

The [lockfile](https://socket.dev/glossary/lockfile) pins files, not just versions. Each entry carries the exact download URL, SHA-256 hash, size, and upload time of every sdist and wheel. Our six-package lock held 116 hashes, most of them platform wheels for charset-normalizer.

uv enforces those hashes. We replaced the requests wheel hash with zeros and synced with an empty cache:

```text
$ uv sync --frozen
error: Failed to download `requests==2.34.2`
  cause: Hash mismatch for `requests==2.34.2`

         Expected:
           sha256:0000000000000000000000000000000000000000000000000000000000000000

         Computed:
           sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0
```

The lockfile protects only the commands that read it without rewriting it. `uv run`, `uv add`, and `uv tree` lock before they act, so editing pyproject.toml and running `uv run` silently re-resolves against the live index. `uv sync --locked` fails instead if uv.lock is out of date with pyproject.toml, and `uv sync --frozen` skips that check entirely ([uv docs](https://docs.astral.sh/uv/concepts/projects/sync/)). CI should use `--locked`.

The hashes prove that a file matches the lock, not that the lock is honest. A malicious pull request or a compromised bot account can edit uv.lock directly. We added nine lines to uv.lock, a new package served from a direct URL with its correct hash plus a dependency edge from requests to it, and left pyproject.toml alone:

```text
$ uv lock --check
Resolved 7 packages in 0.89ms
$ uv sync --locked
 + six==1.17.0 (from https://files.pythonhosted.org/packages/b7/ce/.../six-1.17.0-py2.py3-none-any.whl)
```

Both checks passed, and a plain `uv lock` kept the injected entry. The real requests 2.34.2 has no such dependency. Reviewers are unlikely to notice, because GitHub's Linguist marks uv.lock as generated ([generated.rb](https://github.com/github-linguist/linguist/blob/main/lib/linguist/generated.rb)), which collapses it in pull request diffs. `uv lock --check --refresh` caught it, because `--refresh` discards cached metadata and re-resolves against the index:

```text
$ uv lock --check --refresh
Resolved 6 packages in 129ms
error: The lockfile at `uv.lock` needs to be updated, but `--check` was provided.
```

Run that in CI on every pull request that touches uv.lock, and add `uv.lock -linguist-generated` to `.gitattributes` so the diff is shown. The check needs network access and runs the same resolution code as `uv lock`, so run it without credentials.

A lockfile also cannot help once the file it names turns bad. Astral notes that lockfile URLs point straight at object storage, so a release placed under [PyPI's quarantine status](https://socket.dev/blog/pypi-slashes-malware-response-time) can still install from an existing lock ([Astral](https://astral.sh/blog/uv-audit)). As we argued after [the axios compromise](https://socket.dev/blog/axios-npm-package-compromised), lockfiles make existing installs deterministic; they do not make new resolution safe ([analysis](https://socket.dev/blog/hidden-blast-radius-of-the-axios-compromise)).

## Where package code runs in a uv workflow

Package code can run at four points in a uv workflow, and the first comes before anything is installed. A [wheel](https://socket.dev/glossary/wheel-python-packaging-format) is a prebuilt archive, and installing one copies files without running package code. A source distribution (sdist) has to be built, which means running its build backend, and for a setuptools project that means executing setup.py.

> Four points in a uv workflow where package code can run: locking, syncing, interpreter startup, and uvx tool runs

| Stage | Commands | When package code runs | Effect of no-build |
| --- | --- | --- | --- |
| 1. Lock | uv add, uv lock | An sdist with no static metadata is asked for its dependencies through its build backend | Refuses the build; resolution fails for that package |
| 2. Sync | uv sync, uv run | Wheels run no code. A locked sdist is built when no locked wheel fits the machine | Refuses the build |
| 3. Startup | uv run, python, uv's interpreter probe | A .pth file in site-packages runs at every interpreter start; importing a package runs its module code | None |
| 4. Tools | uvx, uv tool run | Resolves and installs a tool and its dependencies fresh, outside any project lockfile | Applies when passed as a flag or set in config |

Stage 1 surprises people. We added docopt, which publishes only an sdist, and ran `uv lock` with an empty cache. uv called the package's setuptools build hooks, which execute its setup.py, before installing anything:

```text
$ uv lock --refresh -v
DEBUG Preparing metadata for: docopt==0.6.2
DEBUG Calling `setuptools.build_meta:__legacy__.get_requires_for_build_wheel()`
DEBUG Calling `setuptools.build_meta:__legacy__.prepare_metadata_for_build_wheel()`
```

Unlike Poetry, uv has a lock-time switch. With `--no-build`, or `no-build = true` under `[tool.uv]`, uv refused:

```text
$ uv lock --refresh --no-build
hint: Wheels are required for `docopt` because building from source is disabled for all packages (i.e., with `--no-build`)
```

Build backends are a live delivery path. In [the MemTensor compromise](https://socket.dev/blog/memtensor-compromise) on September 23, 2026, the malicious MemoryOS 2.0.34 sdist replaced its standard Poetry build backend with a bundled `sckit_poetry_build.py`, so building that sdist would have run the attacker's file. We think sdist builds are Python's install scripts and deserve the same treatment npm finally gave lifecycle scripts: [npm v12](https://socket.dev/blog/npm-12) made them opt-in in July 2026, after [pnpm 10](https://socket.dev/blog/pnpm-10-0-0-blocks-lifecycle-scripts-by-default) did. uv still builds sdists by default. The cost of refusing is real: packages with no wheel for your platform, and private packages that ship only sdists, fail until you list them in `no-build-package` exceptions.

`no-build` does nothing about stage 3. Python's site module executes any `.pth` line that starts with `import` at every interpreter start. We built a wheel containing one such line, installed it with `uv add --no-build`, and nothing ran. The next `uv run python -c 'print("hello")'` ran it. So did a plain `uv sync` with an empty cache, because uv starts the environment's interpreter to inspect it. [The Hades wave of the Miasma campaign](https://socket.dev/supply-chain-attacks/miasma-mini-shai-hulud-supply-chain-attack) used exactly this path, shipping 37 malicious PyPI wheels with a `*-setup.pth` loader ([our analysis](https://socket.dev/blog/shai-hulud-descends-to-hades-miasma-pypi-wave)).

## Does uv prevent dependency confusion?

uv prevents the classic form of [dependency confusion](https://socket.dev/glossary/dependency-confusion) by default. Its `first-index` strategy takes each package only from the first configured index that has it, so a higher version of your internal package name on PyPI is never considered ([uv docs](https://docs.astral.sh/uv/concepts/indexes/)). pip does the opposite: it merges candidates from `--index-url` and every `--extra-index-url` and takes the best version, the behavior uv offers as `unsafe-best-match` and documents as exposing users to dependency confusion.

In our Poetry tests, an attacker's `acme-billing 99.0.0` on a public index beat the internal 1.2.0 until every internal name was pinned; uv locked the internal versions with no per-package pins. The tighter configuration marks the internal index explicit and pins each internal package to it:

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

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

`first-index` has limits. It trusts index order, and indexes passed on the command line or through `UV_INDEX` take precedence over those in pyproject.toml, so a CI variable can reorder them. It cannot help when the internal index itself proxies PyPI, which moves the confusion into the mirror. Setting `UV_INDEX_STRATEGY=unsafe-best-match` to get past a resolution error switches the protection off. The `explicit` index and per-package sources above hold regardless of order.

## Dependency cooldowns with exclude-newer

A dependency cooldown makes the resolver ignore any file uploaded within a recent window, so a malicious release that is caught and removed quickly never enters your lock. uv added relative durations to `exclude-newer` in 0.9.17 on December 9, 2025 ([changelog](https://github.com/astral-sh/uv/blob/main/changelogs/0.9.x.md)). It works because every file in PyPI's Simple API carries an `upload-time`, standardized in PEP 700, and uv compares each file's upload time individually.

We relocked the demo project with a 30-day [dependency cooldown](https://socket.dev/glossary/dependency-cooldown) on October 15, 2026:

```text
$ uv lock --exclude-newer "30 days"
Resolving despite existing lockfile due to addition of global exclude newer 2026-09-15T22:09:25Z
Resolved 6 packages in 973ms
Updated charset-normalizer v3.5.2 -> v3.5.1
Updated idna v3.20 -> v3.19
```

> Timeline of a 30-day uv cooldown applied on October 15, 2026: files uploaded after the September 15 cutoff are skipped

The cutoff landed on September 15, 2026. idna 3.20 (uploaded September 17) and charset-normalizer 3.5.2 (September 30) fell inside the window, so uv chose idna 3.19 (August 18) and charset-normalizer 3.5.1 (August 15). requests 2.34.2, certifi 2026.7.22, and urllib3 2.8.0 were older than the cutoff and did not change.

The window does not need to be long. In the year to August 2024, PyPI resolved over 90% of malware reports in under 24 hours ([our coverage](https://socket.dev/blog/pypi-slashes-malware-response-time)). In [the PyTorch Lightning compromise](https://socket.dev/supply-chain-attacks/mini-shai-hulud) on April 30, 2026, Socket's AI scanner flagged the malicious versions 18 minutes after publication ([our analysis](https://socket.dev/blog/lightning-pypi-package-compromised)).

Where you set the cooldown matters. uv records it in the lockfile's `[options]` table as `exclude-newer-span = "P30D"`. When we then ran a plain `uv sync` without the flag, uv printed "Resolving despite existing lockfile due to removal of global exclude newer" and deleted the setting from uv.lock. It kept the already-locked versions, so nothing changed yet, but the cooldown was gone: the next `uv lock --upgrade` would take the newest releases. Put it in pyproject.toml, where every developer and CI job inherits it:

```text
[tool.uv]
exclude-newer = "7 days"
# Let one package through for an urgent fix, then remove the override:
# exclude-newer-package = { cryptography = false }
```

uv's cooldown fails closed: files from an index that publishes no upload time are treated as unavailable unless you opt that index out ([uv docs](https://docs.astral.sh/uv/concepts/resolution/)). pip 26.2.1's `--uploaded-prior-to` is "only effective when using indexes that provide upload-time metadata," per its help text. The cost of any cooldown is that security fixes wait too. And unlike [pnpm 11](https://socket.dev/blog/pnpm-11-adds-new-supply-chain-protection-defaults), whose minimum release age defaults to 1,440 minutes ([pnpm docs](https://pnpm.io/settings/dependency-resolution#minimumreleaseage)), uv applies no cooldown unless you configure one.

## uvx resolves outside the lockfile

`uvx` is an alias for `uv tool run`, the Python equivalent of [npx](https://socket.dev/glossary/npx-npm-package-runner): it installs a command-line tool into a temporary environment and runs it. On first use it resolves the latest version of the tool and every dependency against the live index; later runs reuse the cached environment until the cache is pruned or you ask for `tool@latest` ([uv docs](https://docs.astral.sh/uv/concepts/tools/)). None of it appears in your project's uv.lock.

That is the same gap the axios compromise exposed for npx. A pinned `uvx black@26.10.1` fixes black's version, but black declares ranges such as `click>=8.0.0`, so its dependencies resolve to whatever the index served at the moment the cache was filled. The same applies to MCP server configurations and CI jobs that launch tools with `uvx`.

Two settings narrow it. A user-level `~/.config/uv/uv.toml` with `exclude-newer = "7 days"` applies to `uvx` as well as projects. For tools you run repeatedly, `uv tool install` gives a persistent environment that changes only when you run `uv tool upgrade`.

## What uv's built-in checks catch

uv now ships two checks of its own, both previews as of 0.13.1. `uv audit` queries OSV for known vulnerabilities in the locked versions. The malware check, enabled with `UV_MALWARE_CHECK=1`, looks up locked packages against OSV's MAL advisories before syncing. Astral's William Woodruff says that when one matches, "the sync will be terminated before the malicious code has a chance to run" ([Astral](https://astral.sh/blog/uv-audit)).

Both depend on someone else having already reported the package. `uv audit` reports known vulnerabilities, and malware does not get a CVE. A malware advisory exists only after detection, and OSV's feed can also be wrong: on May 26, 2026, [OSV withdrew 157 malware reports](https://socket.dev/blog/osv-withdraws-157-malware-reports) after automated false positives flagged packages including FastAPI. A check that blocks syncs on that feed inherits both its delay and its errors.

uv's own parser has been a target. CVE-2025-54368, fixed in uv 0.8.6 on August 7, 2025, let an attacker "contrive a ZIP that appears harmless to security scanners" while uv extracted different contents, with the archive's hash unchanged ([Astral](https://astral.sh/blog/uv-security-advisory-cve-2025-54368), [GHSA-8qf3-x8v5-2pj8](https://github.com/advisories/GHSA-8qf3-x8v5-2pj8)). PyPI found no exploitation and now rejects such archives. The lesson is general: a matching hash proves you got the file the lock names, not that a scanner and an installer saw the same contents. Analysis has to cover what the installer will actually unpack.

Socket works with uv in two places. Socket Firewall Free wraps uv commands, as in `sfw uv pip install flask`, and runs an ephemeral HTTP proxy that checks each package request against Socket's API, blocking known malware before uv downloads it ([docs](https://docs.socket.dev/docs/socket-firewall-free)). It can only block network requests, and uv's global cache serves hits with none, so run `uv cache clean` once before relying on it. Socket's scans read uv.lock directly, along with [pylock.toml](https://socket.dev/blog/python-adopts-standard-lock-file-format-for-reproducible-installs), the standard lockfile format uv can export ([language support](https://docs.socket.dev/docs/language-support)).

## uv, pip, and Poetry compared

| Control | uv 0.13.1 | pip 26.2.1 | Poetry 2.5.1 |
| --- | --- | --- | --- |
| Lockfile | uv.lock, universal, hashes every file | pip lock writes pylock.toml, marked experimental | poetry.lock, universal, hashes every file |
| Hash verification | Always, from uv.lock | Only in hash-checking mode | Always, from poetry.lock |
| Multiple indexes | First index wins by default | Best version across all indexes | Best version across primary sources |
| Refuse builds while locking | --no-build works at lock time | --only-binary :all: | No lock-time switch |
| Release-age cooldown | exclude-newer, fails closed | --uploaded-prior-to, fails open | solver.min-release-age, fails open |
| Known-malware check | Opt-in preview, OSV MAL | None built in | None built in |
| Trusted publishing | uv publish, with PEP 740 attestations | Not a pip feature; twine or the PyPA action | No native OIDC path |

Our verdict: use uv for new projects, and especially for any project that mixes internal and public indexes, because its defaults fail closed where pip's and Poetry's fail open. Keep pip for one-off installs in throwaway environments, with `--require-hashes` when the requirements are pinned. Keep an existing Poetry project only if you apply the hardening in our Poetry glossary entry.

Every control in that table operates on names, versions, hashes, and timestamps. None of them looks at what a package does. A maintainer account takeover that publishes a malicious release and stays undetected past your cooldown passes every one of them, with a valid hash and, if published through a compromised CI pipeline, valid provenance. No installer closes that gap today. Its job ends at proving the bytes match the lock, and the open problem is deciding, before resolution, whether those bytes should run at all.
