Excerpt: Managing imports and packaging in large monorepos has evolved significantly by 2025, particularly as Python, TypeScript, and polyglot repositories dominate enterprise codebases. This article explores modern best practices for import structure, dependency boundaries, packaging strategies, and tooling integration to ensure maintainability and fast builds in multi-package monorepo environments.
1. Introduction
As software ecosystems grow, monorepos have become a strategic choice for managing large-scale systems that span multiple services, libraries, and languages. However, improper import structures and poor packaging discipline can quickly lead to dependency hell, circular imports, and deployment complexity.
By 2025, frameworks like Bazel, Pants, and Poetry have matured to handle multi-package dependency resolution gracefully. Yet, even with advanced tooling, the engineering discipline around how modules import and package code remains critical.
2. The Problem: Unbounded Imports and Coupling
In monorepos, developers often fall into a trap: since all code lives in one place, it feels natural to import across boundaries. Over time, this creates hidden coupling and brittle builds. A common anti-pattern looks like this:
# common mistake
from core.utils import get_config
from api.models import User
from infra.s3.upload import upload_to_s3 # Cross-layer import β
This style bypasses architectural boundaries and complicates packaging. Instead, each logical package should act as a well-defined dependency with explicit import contracts.
3. Recommended Monorepo Structure
Letβs look at a robust Python-based monorepo structure suitable for both internal services and external library packaging:
monorepo/ βββ pyproject.toml βββ README.md βββ packages/ β βββ core/ β β βββ pyproject.toml β β βββ core/ β β β βββ __init__.py β β β βββ utils.py β βββ api/ β β βββ pyproject.toml β β βββ api/ β β β βββ models.py β βββ infra/ β βββ pyproject.toml β βββ infra/ β β βββ s3.py βββ tools/ βββ lint.py βββ ci_scripts/
Each directory in packages/ is a self-contained Python distribution managed via Poetry or hatchling. Cross-package dependencies are declared explicitly in their pyproject.toml files, ensuring build isolation and consistent dependency management.
4. Import Boundary Rules
Strong import hygiene defines monorepo maintainability. The following principles are widely adopted in high-performing engineering teams:
- Import upward, never sideways. Packages should import from lower layers (e.g., core β infra is fine, but infra β core is not).
- Use absolute imports only. Relative imports (
from .utils import x) create ambiguity in multi-level structures and should be avoided. - Expose stable public APIs. Each package defines an
__init__.pyto expose its API surface. For instance:
# api/__init__.py
from api.models import User
__all__ = ["User"]
This creates a stable, predictable interface for other packages and prevents leaking internal modules.
5. Packaging and Dependency Management
In large monorepos, you must treat internal packages as first-class citizens. Modern tools make this seamless:
| Tool | Purpose | Highlights (2025) |
|---|---|---|
| Poetry | Dependency management | Supports workspaces, package linking, and PEP 621-compliant metadata |
| Pants 2.x | Build orchestration | Optimized caching, parallel builds, fine-grained dependency graphs |
| Bazel | Polyglot build system | First-class Python + TypeScript support with hermetic builds |
| uv (by Astral) | Fast virtualenv + dependency resolver | New standard for local dev environments |
For internal versioning, teams commonly employ semantic-release or Changesets to automate tagging, changelogs, and publishing across multiple packages.
6. Isolation and Testing Strategies
Every package in a monorepo should have its own isolated environment and test suite. Shared integration tests are run at the repository root. For Python, a standard layout might be:
packages/core/tests/ βββ test_utils.py packages/api/tests/ βββ test_models.py
Use pytest with the --import-mode=importlib flag to ensure imports mimic installed-package behavior. Combine with tox or nox to automate multi-package testing:
tox -e core,api,infra
Continuous Integration (CI) pipelines leverage caching between packages using Pants or Bazel incremental builds. Most companies (e.g., Stripe, Netflix, Airbnb) use Pants or custom Bazel wrappers for this level of build optimization.
7. Import Graph Visualization
Visualizing dependencies helps detect unwanted cross-imports and cyclical coupling. A practical workflow involves generating a dependency graph:
pip install pydeps
pydeps packages/api --max-bacon 3 --show-deps
This produces an ASCII-style graph:
ββββββββββββ ββββββββββββ β core βββββββΆβ api β ββββββββββββ ββββββββββββ β² β β βΌ ββββββββββββββΆ infra
Any bidirectional arrows (e.g., core β api) indicate a violation of layering principles.
8. Best Practices Summary
To ensure maintainable import and packaging discipline, the following checklist summarizes best practices:
- Use one tool for dependency resolution – avoid mixing
requirements.txtandpyproject.toml. - Define clear package boundaries via internal namespaces (
org_name.core,org_name.api). - Use explicit dependencies – declare every internal import as a dependency in configuration files.
- Apply import linting with flake8-tidy-imports or isort.
- Pin internal versions when publishing to PyPI mirrors or internal registries.
- Document public APIs using MkDocs or Sphinx auto-imports.
9. Common Pitfalls and How to Avoid Them
Even experienced engineers stumble upon subtle monorepo issues. Here are a few patterns to watch for:
| Issue | Symptom | Mitigation |
|---|---|---|
| Implicit imports via sys.path | Works locally, fails in CI/CD | Use editable installs (pip install -e .) or poetry install |
| Circular dependencies | Imports hang or crash | Refactor shared code into a separate common package |
Overloaded __init__.py |
Long import times, hidden coupling | Keep __init__.py minimal, import explicitly |
| Mixed relative and absolute imports | Runtime ImportError inconsistencies | Standardize on absolute imports only |
10. Future Directions (2025 and Beyond)
The Python packaging ecosystem continues to evolve rapidly. With PEP 735 and PEP 722 discussions introducing per-script dependencies, smaller tooling footprints will soon replace legacy virtualenv-based setups. Tools like uv and Rye are gaining traction for ultra-fast builds and dependency isolation, integrated directly into editors like VS Code and JetBrains PyCharm.
For large organizations, polyrepo-to-monorepo migrations remain a major trend. GitHubβs internal migration to a monorepo (2024β2025) demonstrates that disciplined import layering, combined with robust CI pipelines, enables horizontal scaling without loss of modularity.
11. Conclusion
Good import hygiene and modular packaging are the backbone of maintainable monorepos. Whether using Bazel, Pants, or Poetry, consistency is paramount: each package should feel independent yet play well within the larger system.
When imports are explicit, dependencies declared, and builds reproducible, teams gain confidence to scale both their codebase and their developer velocity. In 2025, the best monorepos are not those with the most automation — but those with the fewest surprises.
Recommended Resources:
