Table of Contents

The Day the Monorepo Fought Back

uv Workspace vs Poetry: managing Python monorepos wasn’t something I planned to write about.
It was something I ended up living through.
The moment is still clear:
a harmless change in one service broke tests in three others.
Not because the code was wrong.
Because the environment graph was wrong.
Same repo.
Same commit.
Different behavior depending on where I ran it.

That’s when I realized something uncomfortable:

My monorepo wasn’t complicated because of code.
It was complicated because of tooling assumptions.


Why Python Monorepos Are Genuinely Hard

A Python monorepo isn’t just “multiple projects in one folder”.

It’s usually:

  • Shared internal libraries
  • Multiple services with overlapping dependencies
  • Different release cadences
  • Partial installs (you rarely need everything)

Most Python tools were designed for a simple assumption:

one project → one environment → one lockfile

Monorepos break that model immediately.


Why I Started With Poetry

I didn’t choose Poetry by accident.

Poetry felt like the right tool:

  • Clean pyproject.toml
  • Strict dependency resolution
  • Lockfile discipline
  • Predictable local environments

At first, it worked.

Then the monorepo grew.


Where Poetry Starts to Struggle

Poetry’s mental model is still fundamentally:

one project, one environment

In a monorepo, this leads to:

  • Multiple poetry.lock files
  • Repeated dependency resolution
  • Long install times in CI
  • Hard-to-explain cross-package behavior
I spent more time explaining why an environment behaved a certain way than fixing actual bugs.

Poetry wasn’t broken.
It just wasn’t built for this shape of repository.


The Breaking Point: Partial Installs

The real pain showed up when I tried something simple:

“I only want to work on the API service, not the whole repo.”

With Poetry, I had to:

  • Install everything
  • Or wire path dependencies manually
  • Or create separate virtualenvs per service

None of that scaled cleanly.


Discovering uv Workspaces

I didn’t switch tools intentionally.
I discovered uv workspaces while optimizing installs elsewhere.
What caught my attention wasn’t speed — it was the mental model.
No orchestration DSL.
No plugin ecosystem.
Just an explicit workspace definition.


The Core Difference: Mental Models

This is the difference that matters:

Poetry thinks in projects

uv thinks in workspaces

That distinction is subtle — and incredibly important for monorepos.


What a uv Workspace Looks Like

At the repository root:

# pyproject.toml
[tool.uv.workspace]
members = [ 
           "libs/common", 
           "services/api", 
           "services/worker" 
]

Each member has its own pyproject.toml.

uv then generates one unified lockfile at the workspace root:

uv.lock

This ensures:

  • Version consistency across all packages
  • No duplicated resolution work
  • Predictable dependency graphs
uv creates one lockfile for the entire workspace, ensuring all packages agree on dependency versions.

Installing Only What You Need

This is where uv won me over.

If the package name in services/api/pyproject.toml is:

[project]
name = "api"

You install just that package with:

uv sync --package api

or the short form:

uv sync -p api
Important: –package uses the package name, not the folder path.

Dependencies resolve once.
Internal libraries are linked automatically.
No full-repo install required.


How Poetry Handles the Same Scenario

Poetry can do this — but with more ceremony.

Example (inside services/api/pyproject.toml):

[tool.poetry.dependencies]
common = { path = "../../libs/common", develop = true }

This works, but it introduces:

  • Fragile relative paths
  • Lockfile drift
  • CI install-order sensitivity

It’s manageable — until the repo scales.


Dependency Resolution: Repeated vs Centralized

This difference compounds over time.

Poetry

  • Resolves dependencies per project
  • Multiple lockfiles repeat work
  • CI pays the cost every time

uv Workspace

  • Resolves once at workspace level
  • Shares results across packages
  • Faster, more predictable installs

CI in a Monorepo (Where This Matters Most)

CI made the contrast obvious.

With uv, I can be explicit:

# Install dependencies for API only
uv sync --package api

# Run API tests only
uv run --package api pytest tests/

Local and CI behavior match exactly.
No hidden environment differences.


Migration Journey (No Big Bang)

I didn’t remove Poetry overnight.

Phase 1: Parallel Setup

I introduced uv alongside Poetry and compared behavior.

Phase 2: One Package at a Time

I migrated a single service and shared library.

Phase 3: Workspace Lockfile

Once stable, uv became the primary resolver.

No freeze weeks.
No forced rewrites.


uv Workspace Limitations (Be Honest)

uv workspaces do have constraints:

  • All workspace members must live inside the workspace root
  • You can’t reference packages outside the workspace
  • One lockfile means coordinated upgrades

For real monorepos, these are usually features, not drawbacks.


When Poetry Still Makes Sense

Poetry is still excellent when:

  • You have a single project
  • You publish directly to PyPI
  • You want strict project isolation
If your repo is not a true monorepo, Poetry may still be the better choice.

When uv Workspace Clearly Wins

uv shines when:

  • You share internal libraries
  • You want partial installs
  • You want one dependency graph
  • You want CI and local to behave identically

That’s most real monorepos.


Final Thoughts

uv Workspace vs Poetry: managing Python monorepos isn’t about declaring a winner.
It’s about choosing the tool whose assumptions match your repository.
Poetry assumes isolated projects.
uv assumes connected ones.
Once my repo crossed that line, the decision became obvious.
I stopped fighting my tooling.
I stopped explaining weird environment behavior.
And my monorepo finally felt… boring.
That’s the best outcome I could ask for.

Categorized in: