EN

United States (EN)

Australia (EN)

Canada (EN)

Canada (FR)

France (FR)

Germany (DE)

Ireland (EN)

Netherlands (NL)

Spain (ES)

United Kingdom (EN)

EN

United States (EN)

Australia (EN)

Canada (EN)

Canada (FR)

France (FR)

Germany (DE)

Ireland (EN)

Netherlands (NL)

Spain (ES)

United Kingdom (EN)

Rippling's Migration to UV from Poetry: Python Dependency Management at Scale

An isometric illustration featuring three square buttons with lightning bolt icons on a dark purple background. The center button is glowing bright pink/purple and connected to circuit board lines, while the two outer buttons are gray and inactive.

Scaling Rippling's engineering team and our 24M-line monolith demanded a faster, more reliable Python dependency manager. When lock times soared past 10 minutes, Nick Grisafi, Senior Software Engineer on Rippling's DevX team, made the move to accelerate.

In just four weeks, Nick and his team successfully migrated the entire Python monolith from poetry to uv and got lock and install times down to under a minute. Read about their migration strategy and initial results below.


Introduction

Rippling has hundreds of engineers contributing to a gigantic Python monolith with 24 million lines of code and tons of third party dependencies. In 2021, we migrated from pip + requirements.txt to poetry. Using pip to manage dependencies was extremely painful for both product and infrastructure teams - without a dependency resolver, adding a new package meant finding the right version range by hand. Without a lockfile we could not guarantee reproducible builds and we had no visibility in the dependency versions we were installing. On top of that, it was slow - taking 450 seconds or more to install all 40 dependencies (roughly 300 including transitive).

Poetry was a major upgrade. Install times dropped to 150 seconds, we finally had a lockfile with reproducible builds, and developers got an intuitive CLI. But Rippling was growing fast. By the start of 2025 - 4 years later - we had 386 dependencies in our pyproject.toml, 628 in our poetry.lock, and the lockfile had ballooned to a gargantuan 16,454 lines. The dependency workflow had become so slow it was unusable and was a conflict-prone tax on everyday development.

In this post, we'll cover how we once again leveled up our Python dependency management - this time by adopting from . We'll walk through the problems that pushed us to migrate, the incremental strategy we used to swap out poetry without disrupting hundreds of engineers, and the results we saw on the other side.

The Problems

Slow dependency operations

With poetry's adoption we had to maintain a lockfile and wait 350 seconds to update a dependency. Poetry version upgrades brought short-lived improvements, but we found ourselves constantly battling the lockfile. By 2025, a lockfile refresh could take 10+ minutes and installs could take ~5 minutes.

Frequent lockfile merge conflicts

Around 21 pull requests could change the lockfile in a given week. poetry.lock merge conflicts had become frequent enough that changes piled up faster than a single pull request could make it through the merge queue. This isn't unique to us - "make the lockfile more merge-friendly" has been a .

A compounding feedback loop

The combination of slow operations and frequent conflicts created a nasty feedback loop. By the time CI finished, the lockfile had often changed again, so engineers would fix conflicts, rerun poetry lock, and wait all over again. Engineers started treating dependency updates like mini-migrations.

Why uv?

Around the time we hit our lockfile inflection point, uv (a newer Python project management tool built in Rust by ) was getting attention for dramatically faster resolves/installs and a lockfile designed to reduce merge conflicts (our two big pain points).

Rippling has been partnered with Astral on getting tools that work at our scale since 2023. We made a successful leap to Astral’s and saw massive performance improvements in lint/format. Astral also released that put our test selection in overdrive, reducing graph generation times from 50 minutes to 1 minute or less. We always worked closely with Astral when adopting their tools, tested on our codebase and provided feedback and bug reports - they were always quick to respond and ready to support our use case. This made us confident in betting on their tooling again.

The Solution 

We migrated from poetry to uv using a dual-run strategy. In Python packaging, a lockfile is the concrete, fully-resolved dependency graph (including transitive dependencies) that tooling uses to produce repeatable installs. We rely on it to keep dev machines, CI, and production in sync and to make dependency changes explicit and reviewable. At Rippling’s scale, even one accidental package upgrade during the migration would be an unacceptable risk. So to reduce risk, we ran poetry and uv together, proved parity, and then flipped the default. During the transition we generated both poetry.lock and uv.lock and resolved diffs (missing packages, version drift, and resolver differences). Alongside the dual-run, we introduced guardrails (linting + standardized CLI commands) to keep the repo compatible and reduce churn.

The outcome was immediate once we completed the cutover: lock and install each dropped to ~1 minute (down from ~10 minutes to lock and ~5 minutes to install at the start of the project).

Media Item | From UV to Poetry 1

How We Did It

Our migration plan was intentionally incremental. Rippling has hundreds of engineers that rely on existing tools and our goal was to keep their developer loop working and cause no impact in production.

Media Item | From UV to Poetry 2 | .PNG

We knew the biggest risk for production was dependency resolution drift between tools. Meaning we can change what we ship in production without realizing it. So we built explicit parity checks to compare poetry.lock to uv.lock.

Milestone 1: Prepare

Before we could leave poetry, we had to upgrade to a newer version. We were pinned to poetry 1.8.2 in the monolith (released March 2024).

Older versions of poetry typically put package metadata (name, version, dependencies) under the poetry-specific [tool.poetry] section in pyproject.toml. The broader Python ecosystem has standardized on , which defines a tool-agnostic [project] table for that same metadata.

Jumping straight from poetry 1.8.2 to uv would have meant changing the package manager tool and migrating our metadata conventions at the same time. That’s too many moving pieces to guarantee no disruption.

Instead, we upgraded poetry in two steps (1.8.2 → 1.8.5 → 2.1.1). Version 1.8.5 was the final release before poetry v2, and poetry v2 supports PEP 621. With poetry v2 we could standardize our pyproject.toml in a way that kept poetry working while we introduced uv in parallel. To prepare for the upgrade, we scrubbed poetry-specific dependency syntax and added a lint to prevent incompatible patterns from creeping back in. The poetry version used to generate poetry.lock was enforced to avoid compatibility issues and unnecessary churn.

Our poetry lint focused on three checks.

  1. Disallowing poetry caret requirements, which can imply broad version ranges and aren’t valid (the standard for dependency specifiers and environment markers).

  2. Disallowing “source” annotations and requiring the default index. In our repo, our internal index was already the default for all packages, so source annotations were unnecessary but if you do need them, uv supports sources per dependency.

  3. Verifying the lockfile was generated with the expected poetry version.

For example, pyproject.toml changes:

Media Item | From Poetry to UV 3

To enforce the poetry version, we used the metadata at the top of the lockfile itself.

This version was extracted and compared to the expected version. Worth noting, uv supports this as a first-class feature via , so you get version enforcement out of the box.

The major version bump came with its gotchas. The biggest change was the removal of the poetry shell command (). The recommendation is the or using the activation script (source .venv/bin/activate) — which is also what poetry env activate prints. The activation script is the way to go because it’s tool-agnostic (it works for both poetry and uv).

There were a few command changes too. poetry install –-sync was deprecated in favor of poetry sync. poetry lock became poetry lock -–regenrate. And poetry lock –-no-update became the default behavior for poetry lock.

Conveniently, these shifts made poetry v2’s line up with the default behavior of uv.

Our developer CLI (lde - local development environment) commands were updated to smooth over these differences. For backwards compatibility, we detected the installed poetry version and chose the correct interface for invocation.

To combat future API changes like this, we introduced a new venv command group to lde and deprecated poetry-flavored commands so engineers had one stable interface while we swapped the engine underneath.

Media Item | To UV From Poetry 5 | .PNG

Some example commands:

At this point we were in the perfect position to bump the major poetry version. We had control over the version the developers used, frequently used commands will not break and we had a lint check to safeguard CI. So we bumped the version and regenerated the lockfile without a hitch.

Once we were on poetry v2, moving to PEP 621 was straightforward. This milestone delivered incremental value as developers got a clearer, more consistent interface for Python dependencies and virtual environments, plus a more modern pyproject.toml that other tooling could understand.

Media Item | To UV From Poetry 7 | .PNG

Again, we landed this metadata update without disrupting developer loops.

Milestone 2: Dual Run

After pyproject.toml metadata was standardized to PEP 621, we reduced tool-specific surface area and made dependency declarations easier to reason about. It also enabled the ability to run both poetry and uv without maintaining two competing configuration formats.

Next, we introduced uv configuration to pyproject.toml.

Media Item | To UV From Poetry 8 | .PNG

All lde commands were wired up to support both uv and poetry. The CLI determined which tool(s) to run based on the presence of poetry.lock and uv.lock.

Media Item | To UV From Poetry 9 | .PNG

For power users, we documented uv equivalents for common operations to reduce confusion during the transition. Concretely, the translations looked like this:

At this stage, uv wasn’t the default installer yet, but we supported it. We checked the uv.lock generation as dependencies changed so we could build confidence before the cutover.

Milestone 3: Parity & Fixes

To ensure parity, the lockfiles must be in sync. To accelerate drift detection, I built an open source tool called that diffs uv.lock and poetry.lock and produces actionable mismatches.

When uv.lock and poetry.lock differed, our playbook was consistent: add missing dependencies explicitly, or pin versions in pyproject.toml when resolution disagreed so production couldn’t surprise us mid-migration. Even better, check whether you can remove the dependency entirely! There were a few of those during our migration.

Milestone 4: Cutover & Cleanup

Once parity checks held up, we switched install flows in scripts/images to use uv.

The install scripts and docker builds on a stricter uv sync invocation:

--locked ensures that the uv.lock file is up to date, and fails the command if not. The failure message will prompt the user to run uv lock.

--active tells uv to use the activated virtualenv. That last bit matters because uv defaults to creating or using a project local .venv. Our docker builds already used a fixed /venv, and uv sync --active let us adopt uv without changing that layout:

If you do need uv itself to target a non-standard venv directory, uv also supports UV_PROJECT_ENVIRONMENT (upstream: ). We didn’t use it in our initial cutover, but it’s a good option and we’ll likely adopt it ourselves over time.

The cutover itself was straightforward. We updated the install scripts and deleted poetry.lock from the trunk. 

After we closely monitored the release for issues. None came up, which gave us high confidence that the tools were in parity and the migration was successful.

Timeline (What This Took For Us)

This is the part most teams worry about: in our case, the migration was weeks, not quarters. The cutover was the easy part, most time went into tool updates.

  • End-to-end elapsed: ~4 weeks from first milestone comms to “uv is live” (mid‑March → mid‑April).

  • M1 (Prepare): ~1 week (poetry v2 + PEP 621 cleanup + guardrails).

  • M2 (Dual Run): ~2 weeks (introduce uv.lock, wire up CLI/install paths, keep locks in sync).

  • M3 (Parity & Fixes): ~1–2 weeks (diffs, pins, internal index/fork drift, upstream fixes).

  • M4 (Cutover & Cleanup): ~1–2 days (flip defaults, delete poetry.lock, ship + monitor).

If you can do a “big bang” migration

If you have a smaller project (or can afford a short freeze), you may be able to migrate in one shot instead of doing an incremental dual-run. A tool worth considering is , which can convert existing poetry / pip-tools / Pipenv / pip project metadata and generate a uv.lock that preserves your previously-locked versions.

For larger repos (or anything safety-critical), an incremental migration like the one in this post is often a safer route. You can run both tools and continuously verify that new changes resolve identically (same versions and sources), then flip the default once you’re confident. It is most valuable when you’re also migrating settings or relying on specific resolution behaviors that can subtly change between tools.

What’s Next

With dependency management no longer a bottleneck, the next opportunity is giving teams more ownership over how they build and deploy. Beyond fast locking and installs, uv also provides advanced project management capabilities like , which let you define multiple interconnected packages with a shared lockfile. We’re now using uv workspaces to offer backend teams a way to construct their own isolated Python projects from their existing Django apps within the monolith. This lets a team scope its dependencies, tests, and deployments to just the code it owns – enabling faster service startup, focused test suites, leaner releases, and more targeted context for AI-assisted development. The end result is smaller deployable units where teams have direct control over their memory footprint, CPU usage, and Docker image sizes. Watch this space for more on that work in a future post.

Conclusion

With uv, our dependency loop went from “schedule time for it” to “just do it”. Now locks and installs are fast, conflicts are rare, and upgrading packages no longer feels like a mini-migration. It pushed us toward cleaner, more standard packaging metadata, which makes the whole ecosystem easier to reason about.

We also want to acknowledge poetry. It gave us a solid, simple foundation for Python dependency management in a large repo, and it paved the way for a lot of the conventions uv builds on. If you’re on poetry today, you’re already on the path that made this migration possible.

Huge credit goes to Astral. Running uv at Rippling’s scale surfaced edge cases, and the Astral team was there to support us. By shipping fixes quickly, clarifying behavior, and improving the tooling in ways that benefit the broader Python community, not just our monolith. That kind of upstream partnership is what makes modern infrastructure feel possible.

If you want the nitty-gritty details, here are a few Astral PRs/issues:

  • uv support for UV_PROJECT_ENVIRONMENT ()

  • read requirements from requires.txt when available ()

  • revert wheel caching without range requests (locking was slower) ()

Now we no longer pay the “slow lockfile tax” when we update a Python dependency.

Disclaimer

Rippling and its affiliates do not provide tax, accounting, or legal advice. This material has been prepared for informational purposes only, and is not intended to provide or be relied on for tax, accounting, or legal advice. You should consult your own tax, accounting, and legal advisors before engaging in any related activities or transactions.

Author

Person in blue shirt and sunglasses next to Pyles Peak Summit sign showing elevation of 1379 ft.

Nick Grisafi

Rippling Engineer and 2024 Quality Week Winner

Nick is a Software Engineer on Rippling’s DevX team, building internal tools to keep hundreds of engineers productive at scale. He’s deeply passionate about Python and obsessed with making developer workflows faster and simpler. (edited)

See Rippling in action

Increase savings, automate busy work, and make better decisions by managing HR, IT, and Finance in one place.