Best practices for imports and packaging in monorepos

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__.py to 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.txt and pyproject.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: