ยท9 min read

The x Pattern: What npx, uvx, bunx, and pipx Actually Do

toolingpythonjavascriptcoding
Terminal window showing npx, uvx, bunx, and pipx commands in glowing cyan text on a dark background
One letter, one big idea: run it without installing it.

At some point, you typed this into your terminal:

npx create-react-app my-app

Probably copied it from a README without thinking too hard about it. It worked, so you moved on. Fair enough. But there's a question hiding in plain sight: what is npx? Why not just npm? And why do Python tutorials now tell you to run uvx instead of pip install? What are bunx and pipx?

They all solve the same problem. I didn't realize this for an embarrassingly long time, so here's the explanation I wish I'd had earlier.

The Problem: Global Installs Are Broken

Say you want to use a CLI tool like create-react-app, eslint, or black (the Python formatter). The old approach was to install it globally:

npm install -g create-react-app
pip install black

This puts the tool on your system PATH so you can run it from anywhere. Sounds reasonable until you actually do it for a while.

Version conflicts. Project A needs eslint@7. Project B needs eslint@8. You can only have one global version. Whichever you installed last wins, and the other project silently breaks.

System pollution. Every global install drops files into a shared directory. Over time, your global node_modules or Python site-packages becomes a graveyard of tools you installed once for a tutorial and never touched again. I ran pip list on a machine I'd been using for two years and found 140+ packages I couldn't account for.

Permission headaches. On many systems, global installs require sudo. Now you're running package managers as root, which is a security anti-pattern and a reliable way to corrupt your system Python.

Reproducibility. You send someone a README that says "first, install these five global tools." They install slightly different versions. Things break in ways that take hours to diagnose.

Global installs share one namespace. When two projects need different versions of the same tool, only one can win.

The "x" Pattern: Run It, Don't Install It

Every major package ecosystem independently landed on the same answer: a runner that downloads a package to a temporary location, executes it, and (optionally) cleans up afterward. Nothing touches your global installs.

The "x" in all of these stands for "execute."

  • npx = node package execute
  • bunx = bun execute
  • pipx = pip execute
  • uvx = uv execute

The mental model:

The lifecycle of an x command: resolve the package, download it to a temp/cache location, run it, done. No global install touched.

You get the tool you need, at the version you need, for as long as you need it.

The Runners, One by One

npx (Node.js / npm)

The one that started it. Shipped with npm 5.2 in 2017. If you have Node.js, you already have it.

# Scaffold a new Next.js project
npx create-next-app@latest my-app
 
# Run a one-off tool without installing it
npx prettier --write "**/*.js"
 
# Run a specific version
npx eslint@8 .

How it decides what to run:

  1. Checks if the package exists in your local node_modules/.bin/
  2. If not, checks the global install
  3. If not found anywhere, downloads it temporarily from the npm registry, runs it, then caches it

This is why npx eslint . works differently depending on whether eslint is in your project's devDependencies or not. If it's local, npx just uses the local one. If not, it fetches it.

pnpm dlx (pnpm)

pnpm's equivalent of npx. It used to be called pnpx, but the current recommended command is pnpm dlx (short for "download and execute").

# Same as npx create-next-app
pnpm dlx create-next-app@latest my-app
 
# Run a one-off tool
pnpm dlx depcheck

Functionally identical to npx, but uses pnpm's content-addressable store. If you already have the package cached from a previous pnpm install, it reuses those files instead of downloading again. Slightly more disk-efficient.

bunx (Bun)

Bun's version. Same idea, but fast. Bun's package resolution is written in Zig and it shows.

# Same thing, noticeably quicker
bunx create-next-app@latest my-app
 
# Run TypeScript files directly (Bun handles TS natively)
bunx tsx my-script.ts

If cold-start speed matters to you, bunx is the one to reach for in the JS ecosystem.

pipx (Python)

pipx was the first mainstream "x" runner for Python (2019). It installs Python CLI tools into isolated virtual environments so they never collide with each other or with your system Python.

# Install a tool in an isolated environment
pipx install black
 
# Run something once without installing
pipx run cowsay "hello from an isolated environment"
 
# Run a specific version
pipx run --spec black==23.1.0 black --check .

Key difference from the JS runners: pipx defaults to persistent isolated installs rather than temporary ones. pipx install black creates a dedicated virtualenv for black that sticks around. pipx run is the one-shot equivalent of npx.

This makes sense for the Python ecosystem, where CLI tools like black, ruff, mypy, and httpie tend to be things you use daily rather than one-off scaffolders.

uvx (Python / uv)

This is the one I reach for most these days. uvx is part of uv, the Rust-based Python package manager from Astral (the Ruff people). Drop-in replacement for pipx run.

# Same as pipx run, but much faster
uvx black --check .
 
# Run with a specific version
uvx ruff@0.3.0 check .
 
# Run a tool that comes from a different package name
uvx --from jupyter-core jupyter

Speed-wise, uv resolves and installs packages so quickly that uvx black . barely feels different from running a locally installed black. The cold-start tax that made pipx feel sluggish is basically gone.

Under the Hood: Where Do the Packages Go?

These runners aren't magic. They download real files to real directories. Knowing where is useful when something breaks or you want to clear the cache.

RunnerCache / Install Location
npx~/.npm/_npx/ (cached packages)
pnpm dlxpnpm's content-addressable store (~/.local/share/pnpm/store/)
bunx~/.bun/install/cache/
pipx~/.local/pipx/venvs/ (one virtualenv per tool)
uvx~/.local/share/uv/tools/ (for persistent installs)

The JS runners tend toward ephemeral caching: download, run, keep it around in case you need it again. The Python runners lean toward persistent isolated environments. This split makes sense when you think about how each ecosystem uses CLI tools. JS devs scaffold projects occasionally. Python devs run formatters and linters daily.

When to Use a Runner vs. When to Install

The runner isn't always the right call. Here's how I think about it:

Use the runner when:

  • You're scaffolding a new project (npx create-next-app, uvx cookiecutter)
  • You need a tool once or rarely
  • You want to try a tool before committing to it
  • You're writing a README and don't want to add "first install these global dependencies" as a prerequisite

Install it properly when:

  • It's a dev dependency for your project (put it in devDependencies / pyproject.toml)
  • You use it every day (black, ruff, eslint in your editor)
  • Speed matters on every invocation (runners have a small resolution overhead)
  • You need reproducible versions locked in a config file
Decision flow: use a package runner for one-off and exploratory use, install as a project dependency for daily tools.

Convergent Evolution

The thing I find genuinely interesting here isn't any individual tool. It's the timeline.

JavaScript got npx in 2017. Python got pipx in 2019. Bun shipped bunx in 2023. uv added uvx in 2024. Four different ecosystems, four different teams, no coordination, same conclusion. When that happens in biology they call it convergent evolution. Same thing applies to tooling: if everyone independently arrives at the same answer, the underlying problem is probably real.

The old model was: install everything, manage it yourself, hope nothing conflicts. The new model is: tools are ephemeral by default and isolated when persistent. Your system is not a junk drawer.

If you're starting a new project today, check whether there's an "x" runner for your ecosystem. There almost certainly is.