I ran my ops repo on Pipenv for a long time. It was a good fit because it combined two things I care about:

  • dependency management
  • runnable scripts in one place

That “deps + scripts” model is still one of Pipenv’s best features.

I migrated anyway because my priorities changed. I wanted a faster Python toolchain and a workflow entry point that is portable across machines and CI. uv and Taskfile deliver that without giving up the one command experience.

TL;DR

I replaced:

  • Pipfile and Pipfile.lock
  • pipenv install and pipenv run
  • Pipfile [scripts]

With:

  • pyproject.toml and uv.lock
  • uv sync and uv run
  • Taskfile tasks

Tools in two paragraphs

uv

uv manages a local virtual environment, resolves dependencies, writes a lockfile, and runs commands inside the environment.

What I use most:

  • uv sync to create or update .venv from uv.lock
  • uv run to execute Ansible and Python tools without activating the venv

The practical benefit is speed. Sync and run are noticeably faster than my old flow.

Taskfile

Taskfile is a small task runner with a static Go binary. It defines workflows as tasks and supports dependencies between tasks.

Why it fits ops repos:

  • one entry point for common actions
  • task dependencies and variables
  • works the same locally and in CI
  • no language runtime required to run the task runner itself

Why I liked Pipenv

I do not consider this a “Pipenv is bad” story. Pipenv did what I needed:

  • it managed the environment
  • it gave me scripts in the same file
  • it kept day to day commands memorable

The reason I moved is not feature parity. It is operational strategy.

Why I moved anyway

The setup I want today optimizes for:

  • fast environment sync and fast command execution
  • a CI agnostic workflow contract for the repo
  • self hosted friendliness and fewer platform assumptions

Hosted runners and policies change. Licensing and usage limits tighten. I want a workflow that still works when convenience goes away. A static task runner plus a fast Python toolchain is a good baseline.

Goals

  • keep one command ergonomics
  • make dependency sync deterministic
  • replace Pipfile scripts with a real workflow layer
  • keep Ansible and linters running inside the same Python environment

The result: uv + Taskfile

After the migration the repo revolves around three files:

  • pyproject.toml for dependencies
  • uv.lock for resolved versions
  • Taskfile.yml for workflows

The mental model is simple:

  • uv owns Python
  • Taskfile owns execution

The Taskfile

This is the shape I ended up with. It keeps deps as a first class task and runs everything through uv run.

# yaml-language-server: $schema=https://taskfile.dev/schema.json

version: "3"

vars:
  playbook: playbook_install.yml

tasks:
  deps:
    sources:
      - pyproject.toml
      - uv.lock
    generates:
      - .venv/pyvenv.cfg
    cmds:
      - uv sync --all-extras --dev
    silent: true
  default:
    deps: [deps]
    generates:
      - $HOME/.gnupg
      - $HOME/.config/kitty
      - $HOME/.config/nvim
      - $HOME/.mplayer
    cmds:
      - uv run ansible-playbook {{.playbook}}

  lint:
    deps: [deps]
    cmds:
      - uv run ansible-lint {{.playbook}}

  configs:
    deps: [deps]
    cmds:
      - uv run ansible-playbook {{.playbook}} --tags configs

  upgrade:
    cmds:
      - uv sync --upgrade

  nvim:
    deps: [default]
    cmds:
      - nvim --headless "+Lazy! restore" +qa
      - nvim --headless "+checkhealth" +qa
      - nvim --headless "+checkhealth vim.lsp" +qa

Why deps is separate

Pipenv made environment setup implicit. I prefer it explicit.

Every task that needs Python depends on deps. That makes failures clearer and keeps the workflow consistent.

Why sources and generates matter

deps has inputs and outputs:

  • sources are pyproject.toml and uv.lock
  • generates is .venv/pyvenv.cfg

That gives Taskfile enough context to skip work when nothing changed.

Migration steps

I did this as “automate first then refine”.

1) Convert the project

From the repo root:

uvx migrate-to-uv

This gave me a starting pyproject.toml and uv.lock.

2) Review dependencies

I checked:

  • which packages belong in main vs dev dependencies
  • which extras I actually want installed

3) Migrate scripts into tasks

Pipfile scripts were convenient but flat. Taskfile tasks became the new interface:

  • task
  • task lint
  • task configs
  • task upgrade

4) Replace pipenv run with uv run

The mapping is direct:

  • pipenv run ansible-playbook becomes uv run ansible-playbook
  • pipenv run ansible-lint becomes uv run ansible-lint

What improved

  • speed in sync and run
  • workflows are explicit and discoverable
  • tasks work locally, on self hosted runners, and across CI systems
  • the repo has a clear contract for “how to operate it”