Skip to content
DevOps AI ToolKit
Newsletter
All guides
AI for Bash & Python Automation By James Joyner IV · · 8 min read

Packaging Python Ops Tools with uv: From 'Works on My Machine' to 'Runs Anywhere'

The handoff from a single script to a shareable tool is where most ops Python rots. uv handles environments, dependencies, and distribution fast — here's how.

  • #python
  • #bash
  • #uv
  • #packaging
  • #dependencies
  • #automation

Every ops team has the same graveyard: a folder of useful Python scripts that only run on the laptop of the person who wrote them. They depend on requests that happens to be installed globally, a Python version that happens to be 3.11, and a pip install someone did once and forgot. The moment a second person tries to run the tool, it breaks. That gap — between a script that works for you and a tool that works anywhere — is where most ops Python goes to die.

uv, the Rust-based Python package manager, closes that gap and is fast enough that you stop dreading the workflow. Here’s how I take an ops script from a throwaway to something a teammate can run with one command.

Why uv specifically

You can do all of this with pip, venv, pip-tools, and pipx stitched together. uv replaces that whole stack with one tool that’s an order of magnitude faster — fast enough that creating a fresh environment for a one-off script is no longer annoying. It manages Python versions, virtual environments, dependency locking, and tool installation. For ops work, where you’re constantly spinning up and tearing down environments, that speed changes your habits.

Install it once:

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

Stage 1: the self-contained script

The lightest possible win — before any packaging — is making a single script declare its own dependencies inline using PEP 723 metadata. uv reads it, builds a throwaway environment, and runs it:

# /// script
# requires-python = ">=3.11"
# dependencies = ["httpx", "rich"]
# ///
import httpx
from rich import print

print(httpx.get("https://example.com/health").json())

Run it with:

uv run health_check.py

No pip install, no virtualenv to remember, no global pollution. uv reads the inline block, resolves and caches the deps, and runs. A teammate with uv installed runs the exact same command and gets the exact same dependencies. This single feature retires most of my “share a script over Slack” friction.

Stage 2: a real project with a lockfile

When a script grows into a tool with multiple files, promote it to a project. uv init scaffolds it:

uv init my-ops-tool
cd my-ops-tool
uv add httpx pydantic-settings typer

uv add does three things: installs the package into a project virtualenv, records it in pyproject.toml, and updates uv.lock — a fully-resolved lockfile pinning every transitive dependency to an exact version and hash. Commit uv.lock. That file is the difference between “it worked in CI but breaks in prod” and reproducible installs everywhere. Anyone (or any pipeline) runs uv sync and gets the identical dependency tree you tested with.

uv sync           # recreate the exact environment from the lockfile
uv run my-tool    # run inside that environment, no activation needed

Notice you never source .venv/bin/activate. uv run handles it. That removes another whole class of “forgot to activate the venv” mistakes.

Stage 3: a proper CLI entry point

To make the tool feel like a real command instead of python main.py, define a script entry point in pyproject.toml:

[project.scripts]
my-tool = "my_ops_tool.cli:main"

That maps the command my-tool to the main() function in my_ops_tool/cli.py. Pair it with a Typer or Click CLI and you’ve got a tool with subcommands, --help, and argument parsing. After uv sync, uv run my-tool --help works.

Stage 4: distribution

Now the payoff — getting it onto other machines. You have two clean paths.

Install as a global tool with uv tool. For CLIs meant to be run by humans, uv tool install puts the command on the PATH in an isolated environment, like pipx:

uv tool install git+https://github.com/yourorg/my-ops-tool
my-tool --help        # now available everywhere, isolated from other tools

Each tool gets its own environment, so two tools needing conflicting dependency versions coexist peacefully. That isolation is exactly what you want for a box that hosts a dozen ops utilities.

Build a wheel for internal distribution. For installing into other projects or a private index:

uv build               # produces dist/*.whl and *.tar.gz

Drop the wheel in an artifact store or private PyPI, and downstream installs are a normal uv pip install or pip install.

Pinning the Python version too

Dependencies aren’t the only thing that drifts — the Python version does too. uv manages interpreters, so you pin Python the same way you pin packages:

uv python pin 3.12

This writes .python-version, and uv will fetch and use exactly that interpreter, downloading it if the machine doesn’t have it. No more “works on 3.11, breaks on 3.9” surprises across your fleet — the runtime is part of the reproducible spec, not an environmental accident.

A workflow that scales with the script

What I like about this progression is that each stage costs almost nothing and you only climb as far as the script warrants:

  • One-off you’ll run twice: inline PEP 723 metadata + uv run.
  • Tool you’ll maintain: uv init + uv.lock, committed.
  • Tool humans invoke: [project.scripts] entry point + uv tool install.
  • Tool that ships internally: uv build + your artifact store.

You never over-engineer a throwaway, and you never under-engineer the thing five people depend on. That match between effort and longevity is exactly what’s missing from the script graveyard — and closing it is mostly about reproducibility: a committed lockfile, a pinned Python version, and a command anyone can run.

For more packaging and tooling patterns, plus the prompts I use to scaffold and document ops tools, see the Bash & Python automation guides and our prompt library.

Commit your lockfile and pin your Python version so the tool you tested is the tool that runs in production.

Free download · 368-page PDF

Download the Free 500-Prompt DevOps AI Toolkit

500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.

  • 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
  • Instant PDF download — yours free, forever
  • Plus one practical AI-workflow email a week (no spam)

Single opt-in · unsubscribe anytime · no spam.