---
description: |
  [TOPIC] Ecosystem Dependency And Version Pinning
  [DETAILS] Dependency hygiene and version-pinning rules across the SciTeX 3-layer cascade — what each package may depend on (upstream only, never downstream), how to declare minima (`>=X.Y` for scitex-* pkgs, exact pin only for security patches), optional-extras pattern (`pip install scitex[plt,stats]`), coordinated release waves so downstream consumers can bump their minima immediately, detection of circular/skipping deps, and the "when you bump, bump consumers' minima" rule. Use when editing any `pyproject.toml`, planning a release wave, or auditing cross-package version drift.
tags: [scitex-general-ecosystem-dependency-and-version-pinning]
---

# Dependency Hygiene & Version Pinning

Companion to [01_ecosystem_01_upstream-and-downstream.md](01_ecosystem_01_upstream-and-downstream.md). The 3-layer cascade imposes strict rules on what each layer may depend on and how versions are pinned.

## Why minima matter — the multi-package development problem

SciTeX is 33+ packages developed **in parallel**, not in lockstep. A feature
added to `scitex-io` on Monday can be used by `scitex-writer` on Tuesday. If
`scitex-writer`'s `pyproject.toml` does NOT declare `scitex-io>=<new>`, this
happens:

```bash
$ pip install scitex-writer      # pip picks any scitex-io >= old minimum
$ python -c "from scitex_writer import foo; foo()"   # AttributeError — new API missing
```

pip cannot know that your new `scitex-writer` silently requires a new
`scitex-io` unless you spell it out. The rule:

> **Every time you import another `scitex-*` package at runtime, pin its
> minimum version (`>=X.Y.Z`) in `pyproject.toml`.**

Without this: every cross-package feature turns into a user-facing
`AttributeError`. With it: `pip install` is the single source of truth for
"which scitex combination is known to work together."

Mechanics (syntax, when to bump, ecosystem-specific layering rules)
follow below.

## Dependency Hygiene

Downstream is **standalone**, not **zero-dep**. Third-party runtime deps (numpy, matplotlib, click, …) are allowed; sibling/middle/upstream SciTeX packages are not, except via optional extras.

| Dep kind | Downstream | Middle | Upstream |
|----------|------------|--------|----------|
| Third-party (numpy, matplotlib, click, …) | ✅ Allowed, **keep minimal** | ✅ Allowed | ✅ Allowed |
| `scitex-dev` (shared infra) | ✅ Allowed (dev tooling / entry points) | ✅ Allowed | ✅ Allowed |
| Sibling downstream (e.g. figrecipe → scitex-writer) | ❌ Not at runtime — only via optional extras | ⚠️ Via plugin registry only | ✅ Allowed |
| Middle (`scitex-io`, `scitex-stats`, …) | ❌ Not at runtime — optional extras only | ✅ Allowed between middle pkgs | ✅ Allowed |
| Upstream (`scitex` only — Axis 1) | ❌ **Never** | ❌ **Never** | ✅ Self |

**Axis 2 packages out of scope.** `scitex-cloud` (user-facing platform) and `scitex-orochi` / `scitex-agent-container` / `scitex-container` (dev tooling & orchestration) sit outside the library cascade — see [`01_ecosystem_01_upstream-and-downstream.md`](01_ecosystem_01_upstream-and-downstream.md) Axis 2A/2B. Library packages do **not** depend on them at runtime (the dependency arrow goes the other way: scitex-cloud *hosts* apps that use the library; scitex-orochi *manages* the cascade). When such a dependency does exist (e.g. an app shipped on scitex-cloud), it lives in the **app's** `pyproject.toml`, not the library's.

### Minimality checklist (downstream)

- [ ] Every runtime dep is actually imported in `src/`.
- [ ] No convenience deps that belong in `[dev]` or `[docs]`.
- [ ] Heavy or rarely-used deps moved to **named extras** (`[imaging]`, `[scientific]`, `[mcp]`, …).
- [ ] Any SciTeX-ecosystem dep is either `scitex-dev` (infra) or listed under an **optional** extra.
- [ ] `pip install <pkg>` in a clean venv produces a working package with no other `scitex-*` installed.

Good example (`figrecipe`): `matplotlib`, `numpy`, `ruamel.yaml`, `scipy`, `click`, `rich` — six tight runtime deps, everything else (Pillow, seaborn, scitex integration) behind extras.

## `[dev]` extras completeness — fastmcp lesson, 2026-05-02

**Rule.** `[dev]` MUST install every dependency that this package's own
test suite imports unconditionally. The only legitimate way to leave
something out of `[dev]` is to also gate the tests with
`pytest.importorskip(...)`. Pick one.

The boundary is **whose feature is being tested**:

| Tested feature lives in… | `[dev]` does | Tests do |
|---|---|---|
| **This package's own `src/`** (e.g. `scitex-notebook`'s MCP server uses `fastmcp`) | Pull the optional 3rd-party dep in so a fresh `pip install -e .[dev]` runs the full suite. **Do NOT** `importorskip`. | Run unconditionally — the feature is yours; commit to testing it. |
| **A sibling `scitex-*` package** (cross-cascade integration test) | Leave the sibling out. Listing it pulls in heavy transitive deps, can shadow editable installs ([03_interface_03_mcp/09 lesson 4](../03_interface_03_mcp/09_lessons-and-pitfalls.md)), and re-introduces lockstep coupling. | `pytest.importorskip("scitex_<sibling>")` — exists when the sibling is around, skips cleanly when not. |
| **A 3rd-party dep this package merely *integrates with*** (e.g. matplotlib plot test) | Pragmatic — include if every CI matrix entry has it; skip if some dimensions deliberately exclude it. | Match the `[dev]` choice. |

**Symmetric pyproject pattern.** When a package has an optional feature
extra `[X]` (e.g. `[mcp]`) AND the test suite covers that feature, the
`[dev]` extra must include the same dep:

```toml
[project.optional-dependencies]
mcp = ["fastmcp>=2.0"]                # production users opt in
dev = [
    "pytest>=7.0", "pytest-cov>=4.0", "ruff",
    # Optional features whose tests live in the suite — installed in
    # [dev] so a fresh `pip install -e .[dev]` runs the full suite.
    "fastmcp>=2.0",
]
```

**Why this matters.** A bare `pip install -e .[dev]` is the canonical
contributor-onboarding command and what every CI workflow runs
([02_package_07_github-actions.md](../02_package_07_github-actions.md)).
If `[dev]` is incomplete, contributors hit `ModuleNotFoundError` at
test-collection time and CI breaks on the first push that touches the
feature. The 2026-05-02 scitex-notebook MCP refactor hit this exact
failure mode on its first push to `develop`.

**Detection.** `audit-project`'s `PS-210` check flags any pyproject extra
whose declared deps are referenced unconditionally from `tests/` but
missing from `[dev]`.

## Optional Dependency Pattern

Downstream packages declare upstream features as **optional extras** so they remain standalone.

### `pyproject.toml`
```toml
[project.optional-dependencies]
scitex = ["scitex[io,session]>=2.24.0"]
all = ["figrecipe[scitex]", "figrecipe[dev]"]
```

### `_AVAILABLE` flags in code
```python
try:
    import scitex as stx
    _SCITEX_AVAILABLE = True
except ImportError:
    _SCITEX_AVAILABLE = False
```

### Clear instructions when deps are missing
```python
def some_feature_requiring_scitex():
    if not _SCITEX_AVAILABLE:
        raise ImportError(
            "This feature requires scitex. "
            "Install it with: pip install figrecipe[scitex]"
        )
```

## Version Pinning Rules

**Principle**: pin the **minimum** version that contains features you rely on. Do **not** pin upper bounds unless a known incompatibility exists. This keeps the ecosystem composable and avoids lockstep upgrades.

### Lower bound: always set it
```toml
# Good
dependencies = [
    "numpy>=1.21.0",         # we use numpy.typing, first in 1.21
    "scitex-io>=0.3.0",      # we call scitex-io.save with new dry_run= kwarg
]

# Bad
dependencies = [
    "numpy",                 # ambiguous — breaks reproducibility of CI
    "scitex-io==0.3.4",      # too tight — blocks consumers
]
```

### Upper bound: only when proven broken
- Add `,<X` **only** when a specific release is known to break, and open an issue to track.
- Prefer fixing forward (new release with `>=Y.Z`) over capping upstream.
- Never cap by default — capping a major version (`<2`) traps consumers.

### When YOU update a package, bump minima in consumers

When you cut `scitex-io 0.4.0` containing a new feature used by `scitex`:

1. In `scitex-io`: bump its own version → `0.4.0`, publish, tag.
2. In every consumer (middle + upstream + downstream that uses it via `[scitex]` extra):
   - Bump its `scitex-io` lower bound to the new minimum that contains the feature.
   - Add a note in the consumer's CHANGELOG linking the feature used.
   - Bump the consumer's own **patch** version (feature now requires newer dep).
3. **Do not** bump minima speculatively — only when you actually use a new API.
4. **Breaking changes** (rename, signature change, removal):
   - Major-bump the producing package.
   - Update every consumer's lower bound **and** code in the same coordinated release wave.
   - Consumers should fail fast on the old minimum rather than silently accept it.

### SciTeX-ecosystem-specific rules

- **Downstream → middle/upstream**: runtime minima live only inside **optional extras** (`[scitex]`). The bare install stays ecosystem-free.
  ```toml
  [project.optional-dependencies]
  scitex = ["scitex-io>=0.4.0", "scitex[session]>=2.24.0"]
  ```
- **Middle → downstream**: minima go under **test** extras (plugin targets for integration tests), not runtime.
  ```toml
  [project.optional-dependencies]
  dev = ["scitex-dev", "pytest>=7.0", "figrecipe>=0.13.0"]  # for cascade tests
  ```
- **Upstream → everything**: minima go under **runtime** deps with matched version ranges.
  ```toml
  dependencies = [
      "scitex-io>=0.4.0",
      "scitex-stats>=0.5.0",
      "figrecipe>=0.13.0",
  ]
  ```
- **Coordinated waves**: when multiple ecosystem packages change together, bump them in one wave with matched minima so a fresh `pip install scitex` resolves cleanly.
- **`scitex-dev ecosystem sync`** (or equivalent) is the canonical tool for fanning minima updates across the ecosystem. Prefer it over hand-editing.

### Quick rule of thumb

> Raise a lower bound **only** when you rely on something that version introduced. Lower it **never**. Cap an upper bound **only** when a release is proven broken.

## Quick Checklist (dependencies & versions)

- [ ] **Downstream**: third-party runtime deps are minimal, justified, and actually imported.
- [ ] **Downstream**: `pip install <pkg>` in a clean venv yields a working package with no other `scitex-*` installed.
- [ ] **Downstream**: tests pass with only `[dev]` extras installed.
- [ ] **Downstream**: every cross-package SciTeX dep lives under an optional extra (`[scitex]`, `[<feature>]`), never in bare `dependencies`.
- [ ] **Middle**: middle→middle deps explicit at runtime; middle→downstream lives under `[dev]` (test/integration only).
- [ ] **All layers**: every dep has a lower bound (`>=X.Y`); no speculative upper bounds.
- [ ] **All layers**: no `==` exact pins outside security patches (and even then, with a tracking issue and an `<X` cap on the next minor).
- [ ] **All layers**: any `<X` upper bound has a linked tracking issue and a follow-up to remove it.
- [ ] **Producers**: when releasing a feature consumers need, bump their minimum **only** in consumers that actually use it.
- [ ] **Producers**: breaking changes major-bump the producer and update every consumer's lower bound *and* code in one coordinated wave.
- [ ] **Axis 2**: no library package depends on `scitex-cloud` / `scitex-orochi` / `scitex-agent-container` / `scitex-container` at runtime.
