How to Build a Scalable Python Monorepo with UV: From Setup to Dependency Management
This tutorial shows how to use the UV tool to create a Python monorepo with separate API, core, and common library projects, manage dependencies with individual pyproject.toml files, and share reusable modules across FastAPI and other services.
Previously I introduced UV , a Python package manager that does more than just install packages. Curious about its capabilities, I used UV to design a monorepo.
Repository Structure
When creating a monorepo I usually include three key projects:
API project : handles various APIs such as FastAPI REST API, gRPC, GraphQL, etc.
Core project : provides the core functionality of the monorepo.
Common library project : a shared library containing utilities, constants, and other common code.
This layout ensures scalability, better code organization, and improved dependency management.
Three Nested Projects
py-api
py-basecamp
py-zcommonlib
py-api and py-basecamp are independent and cannot import each other. Shared functionality should be placed in py-zcommonlib to keep the projects isolated.
Creating the Three Projects with UV
Run the following commands:
<code>uv init py-zcommonlib --lib
uv init py-api
uv init py-basecamp</code>The --lib flag creates a src directory for Python modules; without it, UV defaults to --app , producing a flat structure.
Note: the flat structure is not wrong, just a different approach.
Dependency Management
Each project has its own pyproject.toml , allowing independent and clear dependency control.
Adding a Common Module in py-zcommonlib
Create datetime_lib.py with the following code:
<code>from datetime import datetime, timezone
def get_utc_timestamp():
"""Return the current UTC timestamp in ISO 8601 format.
Example: '2025-02-15T12:34:56Z'"""
return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
if __name__ == "__main__":
print(get_utc_timestamp())</code>Run it with UV:
<code>uv run src/py_zcommonlib/datetime_lib.py</code>Using the Module in py-basecamp
Update main.py to import the function:
<code>from py_zcommonlib.datetime_lib import get_utc_timestamp
def main():
print(get_utc_timestamp())
if __name__ == "__main__":
main()</code>The script fails because py-zcommonlib is not yet listed as a dependency of py-basecamp .
Updating py-basecamp pyproject.toml
Add py-zcommonlib as an editable dependency using a relative path:
<code>[project]
name = "py-basecamp"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.9"
dependencies = ["py-zcommonlib"]
[tool.uv.sources]
py_zcommonlib = { path="../py-zcommonlib", editable=true }</code>Run uv sync in the py-basecamp directory to install the dependency, then uv run main.py to test.
Understanding [tool.uv.sources]
The tool.uv.sources table extends the standard dependency list, allowing alternative sources such as editable installs and relative paths, which are not supported by project.dependencies .
Using the Module in py-api
In the FastAPI project, import and expose the timestamp endpoint:
<code>from fastapi import FastAPI
from py_zcommonlib.datetime_lib import get_utc_timestamp
app = FastAPI()
@app.get("/")
async def utc_timestamp():
return get_utc_timestamp()</code>Corresponding pyproject.toml for py-api :
<code>[project]
name = "py-api"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.9"
dependencies = [
"fastapi>=0.115.11",
"uvicorn>=0.34.0",
"py-zcommonlib",
]
[tool.uv.sources]
py_zcommonlib = { path="../py-zcommonlib", editable=true }</code>Code Mala Tang
Read source code together, write articles together, and enjoy spicy hot pot together.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.