The Modern Way to Install Python CLI Tools Globally Without Breaking PEP 668 Compliance

You’ve built a slick Python CLI tool—clean help messages, intuitive flags, everything just works. Time to install it globally:

pip install .

And then… this happens:

error: externally-managed-environment

× This environment is externally managed
╰─ To install Python packages system-wide, try apt install
    python3-xyz, where xyz is the package you are trying to
    install.

I wrote a blog post about this error, explaining why it exists and how to work around it. But today, I want to focus on a more elegant solution that respects PEP 668 compliance while allowing you to install Python CLI tools globally.

Welcome to the PEP 668 era.

PEP 668 introduced “externally managed environments” to protect your OS from broken dependencies, especially on Linux distributions like Ubuntu 24.04. It’s a good safeguard—but it also means your old pip install habits for global CLI tools no longer work without hacks.

This post isn’t about fighting PEP 668. It’s about working with it—using modern, safe methods to install CLI tools globally without wrecking your system.

The Problem We All Face

Here’s the thing: PEP 668 isn’t trying to ruin your day. It’s actually solving a real problem that anyone who’s maintained Linux systems has experienced:

  • Dependency hell: Your system’s Python packages conflicting with pip-installed ones
  • Broken OS tools: That time apt stopped working because you installed the wrong version of requests
  • The Friday afternoon panic: When your deployment scripts fail because someone globally installed a package that broke system dependencies

But here’s what PEP 668 didn’t solve: how to elegantly install the CLI tools we actually want to use globally. Tools like black, ruff, httpie, or that awesome script you just wrote.

What Actually Works in 2025

Let me save you some time. Here are the approaches that actually work, ranked by how much your future self will thank you:

1. pipx: The Tool You Should Be Using

If you’re not using pipx yet, you’re missing out. It’s specifically designed for this exact problem:

# Install pipx (once)
sudo apt install pipx  # Ubuntu/Debian
brew install pipx      # macOS

# Install CLI tools the right way
pipx install black
pipx install ruff
pipx install your-awesome-cli-tool

What makes pipx brilliant:

  • Each tool gets its own isolated virtual environment
  • Tools are globally accessible from ~/.local/bin
  • Zero dependency conflicts between tools
  • Dead simple management
pipx list                    # See what's installed
pipx upgrade black          # Update one tool
pipx upgrade-all            # Update everything
pipx uninstall black        # Clean removal

2. uv: The Speed Demon

If you haven’t tried uv yet, prepare to have your mind blown. It’s like pipx but written in Rust and ridiculously fast:

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

# Install tools at light speed
uv tool install ruff
uv tool install black
uv tool install your-cli-tool

# Management is equally fast
uv tool list
uv tool upgrade --all

The performance difference is noticeable. What takes pipx 30 seconds, uv does in 3.

3. pip –user: When You Need Simple

Sometimes you just need something installed quickly without the overhead:

pip install --user black

This installs to ~/.local/lib/python3.x/site-packages and puts binaries in ~/.local/bin. It’s fast, simple, and respects PEP 668.

The catch? No isolation. If two tools need conflicting dependencies, you’re back to dependency hell.

4. The Dedicated Virtual Environment Approach

For maximum control, create a dedicated virtual environment for your CLI tools:

# Set up once
python -m venv ~/.venv/cli-tools
source ~/.venv/cli-tools/bin/activate
pip install black ruff httpie your-awesome-tool

# Add to your shell config (~/.bashrc, ~/.zshrc)
alias black='~/.venv/cli-tools/bin/black'
alias ruff='~/.venv/cli-tools/bin/ruff'
alias http='~/.venv/cli-tools/bin/http'

This gives you complete control but requires more setup and maintenance.

Real-World Example: Setting Up a Development Environment

Here’s how I set up CLI tools on a new machine in 2025:

# Install uv (my current preference for speed)
curl -LsSf https://astral.sh/uv/install.sh | sh

# Essential development tools
uv tool install black          # Code formatting
uv tool install ruff           # Linting and more formatting
uv tool install mypy           # Type checking
uv tool install pytest         # Testing
uv tool install cookiecutter   # Project scaffolding
uv tool install httpie         # API testing
uv tool install poetry         # Dependency management

# Verify everything is working
uv tool list

Total time: under 2 minutes. Everything isolated, everything working.

The “But I Have a Script” Problem

“That’s great for published packages, but I just have a Python script I want to install globally.”

I hear you. Here’s the modern approach:

Create a minimal pyproject.toml:

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "my-awesome-script"
version = "0.1.0"
dependencies = []

[project.scripts]
my-script = "my_awesome_script:main"

Then install with pipx or uv tool install .

Option 2: Use pipx run for one-offs

pipx run --spec . my-script

This runs your script without permanently installing it.

Option 3: Direct installation with pipx

pipx install .  # From your script's directory

What About Docker and CI/CD?

In containerized environments, you can often ignore PEP 668 since you control the entire environment:

# In a Dockerfile, this is fine
RUN pip install --break-system-packages my-cli-tool

But even there, consider using uv for speed:

RUN curl -LsSf https://astral.sh/uv/install.sh | sh
RUN uv tool install my-cli-tool

The Migration Path

If you’re currently using --break-system-packages everywhere, here’s your migration strategy:

  1. Audit what you have: pip list --user to see what’s installed
  2. Start fresh: Use pipx or uv tool for new installations
  3. Migrate gradually: Replace --break-system-packages installs one by one
  4. Update your scripts: Change deployment scripts to use modern tools

Performance Comparison

I tested installing 5 common CLI tools on a fresh Ubuntu 24.04 system:

Method Time Isolation Conflicts
uv tool install 12s Perfect None
pipx install 45s Perfect None
pip --user 8s None Possible
Virtual env + pip 15s Perfect None

uv is clearly the speed winner, but pipx has better ecosystem maturity.

What’s Next?

The Python packaging ecosystem keeps evolving. Keep an eye on:

  • PEP 704: Requiring virtual environments (might make this even more important)
  • uv’s rapid development: New features land frequently
  • pipx improvements: Still the most stable choice

Bottom Line

PEP 668 isn’t the enemy—it’s pushing us toward better practices. Instead of fighting it:

  1. Use pipx or uv tool for CLI applications
  2. Embrace isolation instead of global package soup
  3. Update your deployment scripts to use modern tools
  4. Never use --break-system-packages in production

Your future self (and your sysadmin) will thank you. Trust me on this one.