Pular para conteúdo

Python Polylith Monorepo — Reference

1. Core Concepts

Workspace — the monorepo root. One git repo, one virtual environment for development, one workspace.toml. All bricks and projects live here.

Component — an encapsulated, reusable brick of logic. Has a public interface (__init__.py) and private implementation (core.py). Never has a main. Can be used by any base or project. Lives in components/<namespace>/<name>/.

Base — the entry point of a deployable thing (FastAPI app, CLI, Lambda handler, consumer). Thin layer that delegates to components. Lives in bases/<namespace>/<name>/. One base per service. Has core.py with the app/entrypoint.

Project — a deployable artifact. Combines one (or more) bases + selected components into a buildable unit. Has its own pyproject.toml listing which bricks to include. No Python source code goes here — only config and infra (Dockerfile, etc.). Lives in projects/<name>/.

Brick — collective name for components and bases.

Development project — the workspace root itself acts as a unified dev environment. Its pyproject.toml lists all bricks and all dependencies. This is where you run/test everything locally.


2. Directory Structure

my-repo/
├── bases/
│   └── mynamespace/
│       └── greet_api/
│           ├── __init__.py      # exports: from mynamespace.greet_api import core; __all__ = ["core"]
│           └── core.py          # FastAPI app = FastAPI(); @app.get("/") def root(): ...
│       └── worker/
│           ├── __init__.py
│           └── core.py
├── components/
│   └── mynamespace/
│       └── greeting/
│           ├── __init__.py      # exports: from mynamespace.greeting.core import hello_world; __all__ = ["hello_world"]
│           └── core.py          # def hello_world() -> str: return "Hello"
│       └── database/
│           ├── __init__.py
│           └── core.py
├── development/                 # scratch files, notebooks — not deployed
│   └── scratch.py
├── projects/
│   └── my_fastapi_project/
│       └── pyproject.toml       # lists bricks + runtime deps only
├── test/
│   └── mynamespace/
│       └── greeting/
│           └── test_greeting.py
├── pyproject.toml               # workspace root: all bricks + all deps + dev tools
├── workspace.toml               # polylith config: namespace, theme, tag patterns
├── uv.lock
└── mypy.ini

3. workspace.toml

Generated by poly create workspace. Lives at repo root (or embedded in root pyproject.toml under [tool.polylith]).

[tool.polylith]
namespace = "mynamespace"          # single top-level namespace for ALL bricks — choose carefully

[tool.polylith.structure]
theme = "loose"                    # always use "loose" for Python (tdd theme is Clojure-origin)

[tool.polylith.tag.patterns]
stable = "stable-*"
release = "v[0-9]-*"

[tool.polylith.test]
enabled = true                     # generates test stubs with poly create component/base

[tool.polylith.resources]
brick_docs_enabled = false         # set true to auto-generate README per brick

# 1. Init repo
uv init my-repo
cd my-repo

# 2. Add polylith-cli as dev dependency
uv add polylith-cli --dev
uv sync

# 3. Create workspace (sets up folder structure + workspace.toml)
uv run poly create workspace --name mynamespace --theme loose

# 4. Configure pyproject.toml build backend so uv knows where source lives

Root pyproject.toml (workspace level):

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "my-repo"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = [
    "fastapi",
    "uvicorn",
    # all runtime deps for all services go here for dev
]

[dependency-groups]
dev = [
    "polylith-cli>=1.43.1",
    "mypy",
    "pytest",
    "ruff",
]

[tool.uv.workspace]
members = ["projects/*"]          # treat projects as uv workspace members

[tool.hatch.build]
dev-mode-dirs = ["components", "bases", "development", "."]
# critical: tells hatchling/uv where to find brick source code

[tool.uv]
dev-dependencies = ["polylith-cli>=1.43.1"]

After editing pyproject.toml: uv sync to rebuild the venv.


5. CLI Commands

# --- CREATE ---
uv run poly create workspace --name <namespace> --theme loose
uv run poly create component --name <name>     # creates components/<ns>/<name>/
uv run poly create base --name <name>          # creates bases/<ns>/<name>/
uv run poly create project --name <name>       # creates projects/<name>/pyproject.toml

# --- INSPECT ---
uv run poly info                    # table: all bricks × projects, what uses what
uv run poly info --short            # compact version
uv run poly deps                    # brick dependency graph
uv run poly deps --brick greeting   # deps for a specific brick
uv run poly libs                    # third-party lib usage per brick
uv run poly libs --strict           # stricter analysis

# --- VALIDATE ---
uv run poly check                   # validate all declared deps match actual imports
uv run poly check --directory projects/my_fastapi_project  # check a single project

# --- DIFF (git-based) ---
uv run poly diff                    # what changed since last stable-* tag
uv run poly diff --bricks           # list changed bricks only
uv run poly diff --since v1-0       # diff against a specific tag

# --- SYNC ---
uv run poly sync                    # auto-add missing bricks to projects' pyproject.toml
                                    # based on actual imports; run after adding imports

# --- BUILD (uv requires manual setup/teardown) ---
uv run poly build setup             # copies bricks into project dir for build
cd projects/my_fastapi_project && uv build
cd ../.. && uv run poly build teardown  # cleans up copied files

With Poetry, prefix with poetry run or poetry poly. With Hatch: hatch run poly.


6. Namespace Convention

  • One namespace per workspace — every component and base shares the top-level package name.
  • Set once in workspace.toml under namespace = "mynamespace". Never change it.
  • Directory layout enforces it: components/mynamespace/greeting/, bases/mynamespace/greet_api/.
  • Import pattern everywhere: from mynamespace.greeting import hello_world
  • Follow PEP 423 for naming: lowercase, short, no hyphens (use underscores).
  • For PyPI publishing from the same monorepo, use --with-top-namespace flag (rewrites imports via AST) to avoid namespace collisions between published packages.

7. Component Interface Pattern

Generated structure for poly create component --name greeting:

components/mynamespace/greeting/
├── __init__.py   ← PUBLIC interface
└── core.py       ← private implementation

__init__.py — re-exports only the public API:

from mynamespace.greeting.core import hello_world

__all__ = ["hello_world"]

core.py — implementation:

def hello_world() -> str:
    return "Hello, World!"

Consumers always import from the interface, never from core directly:

from mynamespace.greeting import hello_world   # correct
from mynamespace.greeting.core import hello_world  # avoid

8. Adding a Component as a Dependency to a Project

Project's pyproject.toml at projects/my_fastapi_project/pyproject.toml:

[build-system]
requires = ["hatchling", "hatch-polylith-bricks"]
build-backend = "hatchling.build"

[project]
name = "my-fastapi-project"
version = "0.0.1"
requires-python = ">=3.12"
dependencies = [
    "fastapi",
    "uvicorn",
    # ONLY runtime deps this project actually needs
]

# Required to activate the polylith build hook
[tool.hatch.build.hooks.polylith-bricks]

# Required for building a wheel
[tool.hatch.build.targets.wheel]
packages = ["mynamespace"]

# THE KEY SECTION — maps relative paths to brick import paths
[tool.polylith.bricks]
"../../bases/mynamespace/greet_api" = "mynamespace/greet_api"
"../../components/mynamespace/greeting" = "mynamespace/greeting"
"../../components/mynamespace/log" = "mynamespace/log"

For Poetry projects, use packages in [tool.poetry] instead:

[tool.poetry]
packages = [
    {include = "mynamespace/greet_api", from = "../../bases"},
    {include = "mynamespace/greeting", from = "../../components"},
]

poly sync auto-populates [tool.polylith.bricks] by scanning actual imports — run it after wiring up new components.


9. FastAPI Base + FastHTML Base in the Same Workspace

Both coexist as separate bases under the same namespace:

bases/mynamespace/
├── greet_api/          ← FastAPI service
│   ├── __init__.py
│   └── core.py
└── web_ui/             ← FastHTML app
    ├── __init__.py
    └── core.py

bases/mynamespace/greet_api/core.py:

from mynamespace import greeting
from mynamespace.log import get_logger
from fastapi import FastAPI

logger = get_logger("greet-api")
app = FastAPI()

@app.get("/")
def root() -> dict:
    logger.info("root called")
    return {"message": greeting.hello_world()}

bases/mynamespace/web_ui/core.py:

from fasthtml.common import *
from mynamespace import greeting

app, rt = fast_app()

@rt("/")
def index():
    return Titled("Home", P(greeting.hello_world()))

Separate projects for each:

projects/
├── fastapi_service/
│   └── pyproject.toml   # bricks: greet_api + greeting + log; deps: fastapi, uvicorn
└── web_ui_service/
    └── pyproject.toml   # bricks: web_ui + greeting; deps: python-fasthtml

Shared components (greeting, log, database) are listed in both projects' [tool.polylith.bricks].


10. Running / Developing Locally

Everything runs from the workspace root using the single shared venv:

# FastAPI
uv run uvicorn mynamespace.greet_api.core:app --reload

# FastHTML
uv run python -m mynamespace.web_ui.core

# or with fastapi dev runner
uv run fastapi dev bases/mynamespace/greet_api/core.py

# Tests (all)
uv run pytest

# Tests for changed bricks only
changes="$(uv run poly diff --bricks --short)"
uv run pytest -k "${changes//,/ or }"

The development/ folder is for scratch code and notebooks — it has full access to all bricks and deps but is never included in any project build.


11. mypy / IDE Configuration

mypy.ini at workspace root:

[mypy]
mypy_path = components, bases
namespace_packages = True
explicit_package_bases = True

VS Code / Pyright (pyproject.toml):

[tool.pyright]
extraPaths = ["bases", "components"]

VS Code settings.json:

{
  "python.analysis.extraPaths": ["bases", "components"]
}

pytest (for namespace package resolution):

[tool.pytest.ini_options]
addopts = ["--import-mode=importlib"]
consider_namespace_packages = true

12. Key Gotchas and Best Practices

Namespace is permanent. Changing it after the fact requires renaming every directory and every import. Choose once, choose well.

No Python code in projects/. Projects are config/infra only. All logic lives in components and bases.

Components must not import from bases. Data flows one way: components ← bases. A component importing a base is an architecture violation (poly check will flag it as circular or illegal).

dev-mode-dirs is mandatory. Without [tool.hatch.build] dev-mode-dirs = ["components", "bases", "development", "."], uv/hatch won't resolve brick imports. This is the #1 setup mistake.

poly check vs poly libs. poly libs analyzes third-party imports. poly check validates that all declared bricks in pyproject.toml match what's actually imported. Run both before building.

poly sync doesn't add third-party deps. It only syncs brick entries in [tool.polylith.bricks]. You must manually add PyPI deps to each project's [project].dependencies.

PyCharm file moves. When moving files between bases/ and components/, uncheck "Search for references" in PyCharm's move dialog — it can create stray __init__.py files that corrupt namespace package resolution.

Don't add __init__.py to bases/ or components/ top-level. The namespace package mechanism requires these folders to have no __init__.py at the namespace level (bases/mynamespace/ has no __init__.py; only bases/mynamespace/greeting/ does).

poly diff needs git tags. poly diff compares against the most recent stable-* tag. Without a tag, it reports everything as changed. Tag your stable commits: git tag stable-1.

Project pyproject.toml = production deps only. Root pyproject.toml has everything (for dev). Project pyproject.toml has only what that service needs at runtime.

uv build needs setup/teardown. Unlike Poetry and Hatch, uv's build backend has no hook support — run poly build setup before uv build and poly build teardown after, or the wheel will be incomplete.