Skip to content

How to Use uv exclude-newer for PyPI Supply Chain Security

Problem

I woke up to a security alert. A package I installed yesterday had been flagged as malicious on PyPI. The attacker had uploaded a compromised version of a popular library just 6 hours before I ran my uv lock command.

Security Alert Email
Subject: PyPI Security Advisory - Malicious Package Detected
Package: requests-helper v2.1.0
Uploaded: 2026-04-01 14:30 UTC
Detected: 2026-04-01 20:45 UTC
Status: Removed from PyPI
Your project last resolved dependencies: 2026-04-01 15:00 UTC
Action Required: Re-run uv lock immediately

The timing was terrifying. The malicious package was uploaded, and within 90 minutes my CI/CD pipeline had locked it into my project. I had no defense against this.

This got me thinking: how do I protect against packages that haven’t been vetted yet?

What’s Happening?

PyPI supply chain attacks follow a predictable pattern. Attackers know that newly published packages have a window of vulnerability - the time before the community detects and removes them.

Supply Chain Attack Timeline
┌─────────────────────────────────────────────────────────────────┐
│ Attack Lifecycle │
├─────────────────────────────────────────────────────────────────┤
│ T+0h Malicious package uploaded to PyPI │
│ T+1h Automated scanners start analyzing │
│ T+2h Some users already installed the package │
│ T+4h Community reports start appearing │
│ T+6h Package flagged, PyPI administrators notified │
│ T+8h Package removed from PyPI │
│ T+10h Security advisories published │
│ │
│ The vulnerable window: T+0h to T+6h │
│ Most attacks detected within 24-48 hours │
└─────────────────────────────────────────────────────────────────┘

The critical insight: most malicious packages are caught within hours or days. If I could just wait before consuming new packages, I’d let the community do the vetting for me.

A Reddit discussion on r/Python confirmed this approach. The top comment with 76 upvotes was straightforward:

“For uv, in pyproject.toml: [tool.uv] exclude-newer = '2 weeks'

Another comment added context: “Use exclude-newer type of feature to avoid consuming the very latest packages - most vulnerabilities are found in a few hours.”

The community consensus was clear: a dependency cooldown is the solution.

The Solution: exclude-newer

uv’s exclude-newer configuration creates a “dependency cooldown.” It tells uv to ignore any package version published within a specified time window.

Let me try the basic configuration:

pyproject.toml
[tool.uv]
exclude-newer = "2 weeks"

Now when I run uv lock, it will only consider packages published more than 14 days ago:

terminal
$ uv lock
Resolved 42 packages in 234ms
# All packages are at least 2 weeks old
# Any package published in the last 14 days is excluded

This gives the community time to detect and remove malicious packages before I even consider them.

How It Works

Let me trace through what happens when uv resolves dependencies with exclude-newer:

exclude-newer Resolution Flow
┌─────────────────────────────────────────────────────────────────┐
│ uv lock with exclude-newer │
├─────────────────────────────────────────────────────────────────┤
│ 1. Parse exclude-newer configuration │
│ └─ Convert "2 weeks" to cutoff timestamp │
│ └─ Example: 2026-04-02 - 14 days = 2026-03-19 │
│ │
│ 2. Query PyPI for package metadata │
│ └─ Fetch upload timestamps for all versions │
│ │
│ 3. Filter available versions │
│ └─ Exclude versions uploaded after cutoff │
│ └─ Only consider "vetted" packages │
│ │
│ 4. Resolve dependencies │
│ └─ Use SAT solver on filtered versions │
│ └─ Generate uv.lock with safe packages │
└─────────────────────────────────────────────────────────────────┘

The cutoff calculation happens automatically:

Cutoff Timestamp Example
Configuration: exclude-newer = "2 weeks"
Current time: 2026-04-02 11:32:00 UTC
Cutoff time: 2026-03-19 11:32:00 UTC
Package versions EXCLUDED (published after cutoff):
- requests v2.32.0 (published 2026-03-25) -> EXCLUDED
- numpy v2.2.0 (published 2026-03-20) -> EXCLUDED
Package versions INCLUDED (published before cutoff):
- requests v2.31.0 (published 2026-03-01) -> INCLUDED
- numpy v2.1.0 (published 2026-03-15) -> INCLUDED

Why This Matters

Traditional security approaches focus on vulnerability scanning AFTER installation. exclude-newer shifts security LEFT:

Security Approach Comparison
┌─────────────────────────────────────────────────────────────────┐
│ Traditional Approach (Reactive) │
├─────────────────────────────────────────────────────────────────┤
│ 1. Install packages │
│ 2. Run vulnerability scanner │
│ 3. Detect issues │
│ 4. Update/remove problematic packages │
│ │
│ Problem: You already installed the malicious package │
│ Problem: Scanner might not detect new attacks │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ exclude-newer Approach (Proactive) │
├─────────────────────────────────────────────────────────────────┤
│ 1. Set cooldown period │
│ 2. Resolve dependencies (new packages excluded) │
│ 3. Install vetted packages only │
│ 4. Community has already caught malicious packages │
│ │
│ Benefit: Malicious packages never enter your project │
│ Benefit: Leverage community vetting as free security layer │
└─────────────────────────────────────────────────────────────────┘

This is particularly important because:

  1. Lockfiles alone don’t protect against new attacks - A fresh uv lock can pick up a malicious package uploaded 5 minutes ago
  2. Pinning versions requires discipline - Casual uv lock --upgrade can expose you to new attacks
  3. Cooldown provides automatic protection - No manual intervention needed

Configuration Options

pyproject.toml
[tool.uv]
exclude-newer = "2 weeks"

Supported time units: seconds, minutes, hours, days, weeks

I recommend 1-2 weeks for most projects. This balances security with practical dependency updates.

pyproject.toml
[tool.uv]
exclude-newer = "2026-03-15T00:00:00Z"

Use RFC 3339 format for exact reproducibility. This is ideal for production releases where you want to pin to a specific vetting point.

Per-Package Override

Some packages update frequently and have strong security practices. You can apply different cooldowns per package:

pyproject.toml
[tool.uv]
exclude-newer = "1 week"
exclude-newer-package = { setuptools = "30 days" }

This applies a stricter 30-day cooldown to setuptools while using 1 week for other packages.

CLI Usage

You can also use exclude-newer directly from the command line:

terminal
# Lock with a specific cutoff date
uv lock --exclude-newer "2026-03-15T00:00:00Z"
# Run with locked dependencies
uv run --locked python main.py

Inline Script Configuration

For standalone Python scripts with inline dependencies (PEP 723), you can also set exclude-newer:

script.py
# /// script
# dependencies = [
# "requests",
# "pandas",
# ]
# [tool.uv]
# exclude-newer = "2026-03-15T00:00:00Z"
# ///
import requests
import pandas as pd
def fetch_data():
response = requests.get("https://api.example.com/data")
return pd.DataFrame(response.json())
if __name__ == "__main__":
df = fetch_data()
print(df.head())

Run it with:

terminal
$ uv run script.py
# uv respects the inline exclude-newer configuration

My Trial-and-Error Process

I didn’t get this right the first time.

Attempt 1: Too Short Cooldown

pyproject.toml (WRONG)
[tool.uv]
exclude-newer = "1 day"

A 1-day cooldown provides minimal protection. Most attacks are detected within 24-48 hours, but some take up to a week. I was still exposed.

Attempt 2: Forgot to Update Gradually

pyproject.toml (PROBLEM)
[tool.uv]
exclude-newer = "2026-01-01T00:00:00Z"

I set a fixed date and forgot about it. Three months later, my project was stuck on ancient packages. Missing legitimate security fixes.

The fix: set reminders to periodically update:

terminal
# Every 2 weeks, bump the cutoff forward
# From: exclude-newer = "2026-01-15"
# To: exclude-newer = "2026-01-29"

Or use relative time:

pyproject.toml (CORRECT)
[tool.uv]
exclude-newer = "2 weeks" # Automatically recalculated each lock

Attempt 3: Didn’t Combine with Lockfile

terminal (WRONG)
$ uv lock
$ git add pyproject.toml
$ git commit -m "Add exclude-newer"
# Forgot to commit uv.lock!

The exclude-newer setting determines WHAT packages are allowed, but uv.lock records WHICH specific versions. Without committing the lock file, my CI/CD might resolve different versions.

terminal (CORRECT)
$ uv lock
$ git add pyproject.toml uv.lock
$ git commit -m "Add exclude-newer with lock file"

Common Mistakes

Mistake 1: Setting Cooldown Too Short

pyproject.toml
# WRONG - Minimal protection
exclude-newer = "1 day"
# CORRECT - Meaningful protection
exclude-newer = "2 weeks"

Most malicious packages are detected within 1-2 weeks. A shorter cooldown barely helps.

Mistake 2: Not Committing Lock Files

terminal
# WRONG - Lock file not committed
$ echo "uv.lock" >> .gitignore
# CORRECT - Always commit lock file
$ git add uv.lock

exclude-newer works with lock files. Without the lock file, protection is incomplete.

Mistake 3: Overriding Without Justification

pyproject.toml
# WRONG - Why does requests need a shorter cooldown?
[tool.uv]
exclude-newer = "2 weeks"
exclude-newer-package = { requests = "1 day" }
# CORRECT - Only override when necessary
[tool.uv]
exclude-newer = "2 weeks"
# No overrides unless you have a specific reason

Per-package overrides should be rare and justified.

Mistake 4: Mixing with Regular Upgrade Habits

terminal
# WRONG - Bypasses your protection
$ uv lock --upgrade
# CORRECT - Respect the cooldown
$ uv lock # No --upgrade flag

The --upgrade flag can override exclude-newer behavior. Use it carefully.

The Workflow I Now Use

After my security scare, I established this workflow:

My Secure Dependency Workflow
┌─────────────────────────────────────────────────────────────────┐
│ Project Setup │
├─────────────────────────────────────────────────────────────────┤
│ 1. Initialize project │
│ $ uv init my-project │
│ │
│ 2. Add exclude-newer to pyproject.toml │
│ [tool.uv] │
│ exclude-newer = "2 weeks" │
│ │
│ 3. Add dependencies │
│ $ uv add requests pandas numpy │
│ │
│ 4. Commit everything │
│ $ git add pyproject.toml uv.lock │
│ $ git commit -m "Initialize with exclude-newer" │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ Regular Updates │
├─────────────────────────────────────────────────────────────────┤
│ 1. Every 2 weeks: │
│ $ uv lock # Cutoff automatically moves forward │
│ $ git add uv.lock │
│ $ git commit -m "Update dependencies" │
│ │
│ 2. If a critical security fix is needed: │
│ $ uv add package@specific-version │
│ # Override exclude-newer for this specific version │
└─────────────────────────────────────────────────────────────────┘

Why This Matters for Teams

The exclude-newer approach scales well for teams:

Team Benefits
1. Consistent Security Standard
- Every team member has the same cooldown
- No one accidentally installs fresh packages
2. Reproducible Builds
- Same cutoff timestamp = same package versions
- CI/CD uses identical dependencies as developers
3. Reduced Security Alert Response Time
- When alerts arrive, you're likely already safe
- No emergency "did we install that?" investigations
4. Clear Update Policy
- "We wait 2 weeks before adopting new versions"
- Simple rule everyone understands

The Reason This Feature Exists

uv’s developers at Astral recognized that the traditional dependency model was optimized for convenience, not security. In the early Python days, PyPI was small and trusted. Today, with thousands of packages and frequent attacks, blind trust is dangerous.

The design philosophy:

exclude-newer Design Principles
1. Security through delay, not through scanning
2. Leverage community vetting as a free security layer
3. Make security automatic, not manual
4. Balance protection with practical workflow

This aligns with how other ecosystems think about security. Debian’s “stable” releases use similar approaches - packages are vetted over time before inclusion.

Summary

uv’s exclude-newer creates a dependency cooldown that protects against PyPI supply chain attacks by ignoring packages published within a specified time window. The recommended configuration:

pyproject.toml
[tool.uv]
exclude-newer = "2 weeks"

This configuration:

  • Gives the community 14 days to detect and remove malicious packages
  • Maintains reproducible builds with committed lock files
  • Balances security with practical dependency updates
  • Works automatically without manual intervention

After my security scare, I now use this in every project. The next time a malicious package is uploaded to PyPI, I’ll have a 2-week buffer before it can even enter my consideration set. That’s the kind of security that lets me sleep better.

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