Skip to content

feat: make numpy an optional dependency - #34

Open
isaacbmiller wants to merge 17 commits into
mainfrom
isaac/numpy-opt
Open

feat: make numpy an optional dependency#34
isaacbmiller wants to merge 17 commits into
mainfrom
isaac/numpy-opt

Conversation

@isaacbmiller

@isaacbmiller isaacbmiller commented May 7, 2026

Copy link
Copy Markdown

Summary

Move numpy from a required runtime dependency to an optional extra.

numpy-dependent features (KNN, Embeddings, SIMBA, MIPROv2, DummyVectorizer, Embedder post-processing, etc.) raise a clear ImportError pointing at pip install dspy[numpy] only when actually invoked.

Changes

New: dspy/utils/lazy_import.py

Centralized lazy-import infrastructure (ported from dspy-core e8e3e196):

  • require(module, *, extra, feature) — import or raise a friendly ImportError with install hint
  • optional(module, attr, default) — import-or-default for module-level sentinels
  • is_available(module) — cheap find_spec-based check, lru_cached
  • _INSTALL_HINTS registry mapping import names to dspy extras (numpy, optuna, mcp, langchain, weaviate, anthropic)

Replaced numpy with stdlib where possible

  • math.log2 in mipro_optimizer_v2, gepa
  • math.inf in infer_rules
  • statistics.pstdev in copro_optimizer
  • sum()/len() in teleprompt/utils
  • Removed dead np.random.seed() in mipro (optuna uses its own seed param)

Lazy numpy via require("numpy")

Files that genuinely need numpy arrays (knn.py, embeddings.py, embedding.py, dummies.py, simba.py) now call require("numpy") inside method bodies instead of top-level import numpy.

pyproject.toml

  • Removed numpy from hard dependencies
  • Added numpy = ["numpy>=1.26.0"] optional extra
  • Added numpy to dev and test_extras so CI still gets it

Tests

  • Added tests/utils/test_lazy_import.py — 10 tests covering require, optional, is_available, registry/fallback behavior, and pyproject extras consistency check (Python 3.10 compatible)

Notes

@greptile-apps

greptile-apps Bot commented May 7, 2026

Copy link
Copy Markdown

Greptile Summary

This PR makes numpy an optional dependency by introducing a lazy_import.py helper and replacing direct numpy usage with either stdlib equivalents (math.log2, math.inf, statistics.pstdev, sum/len) or a require("numpy") stub that defers the ImportError with a pip install dspy[numpy] hint until the feature is actually used.

  • dspy/utils/lazy_import.py (new): require() returns a LazyLoader-wrapped module when the package is installed, or a _MissingModule stub that raises ImportError with an install hint on first attribute access; is_available() provides a cached find_spec check without triggering an import.
  • Stdlib substitutions: np.log2math.log2, -np.inf-math.inf, np.stdstatistics.pstdev, np.averagesum/len across mipro_optimizer_v2, infer_rules, copro_optimizer, gepa, and utils.
  • from __future__ import annotations correctly added to embedding.py, dummies.py, embeddings.py, and simba.py — preventing evaluation of -> np.ndarray return annotations at class-definition time when numpy is absent.

Confidence Score: 5/5

Safe to merge. All numpy usages are either replaced with stdlib equivalents or guarded by the new lazy-import infrastructure, and CI still receives numpy via the updated dev/test_extras groups.

The stdlib substitutions are semantically equivalent, the lazy-import machinery follows a well-established pattern (LazyLoader + stub), and from __future__ import annotations is applied exactly where needed to prevent numpy annotation evaluation at class-definition time. All previously raised issues have been addressed in this diff.

No files require special attention.

Important Files Changed

Filename Overview
dspy/utils/lazy_import.py New centralized lazy-import infrastructure: require() returns a LazyLoader-wrapped module or a _MissingModule stub that defers ImportError with install hints; is_available() uses a cached find_spec check.
dspy/clients/embedding.py Replaced hard import numpy as np with module-level np = require("numpy"); added from __future__ import annotations to prevent evaluation of -> np.ndarray return annotation at class-definition time when numpy is absent.
dspy/utils/dummies.py Same lazy-require pattern; from __future__ import annotations is necessary because DummyVectorizer.__call__ carries a -> np.ndarray return annotation.
dspy/retrievers/embeddings.py Lazy-require pattern applied; from __future__ import annotations defers evaluation of np.ndarray parameter annotations in _faiss_search, _rerank_and_predict, and _normalize.
dspy/predict/knn.py Replaced hard numpy import with module-level require("numpy"); no numpy type annotations so from __future__ import annotations is correctly omitted.
dspy/teleprompt/copro_optimizer.py All three np.std calls replaced with statistics.pstdev; semantically equivalent (both compute population std dev). Guarded numpy import removed cleanly.
dspy/teleprompt/mipro_optimizer_v2.py np.log2math.log2 and np.random.seed removed; numpy dependency fully eliminated from this file.
dspy/teleprompt/utils.py np.array/np.average replaced with list + sum/len; np.sum replaced with sum(); int() cast removed (no longer needed since builtin sum returns a Python int).
dspy/teleprompt/infer_rules.py -np.inf replaced with -math.inf; top-level numpy import removed entirely.
dspy/teleprompt/gepa/gepa.py Local import numpy as np inside auto_budget replaced with math.log2; numpy dependency removed from this method.
dspy/teleprompt/simba.py Module-level require("numpy") pattern applied; from __future__ import annotations added for consistency with other files using union-type hints.
tests/utils/test_lazy_import.py 10 focused unit tests for require, is_available, and _MissingModule; pyproject extras consistency check implicitly relies on tomli being installed as a pytest transitive dependency on Python 3.10.
pyproject.toml numpy removed from hard dependencies, added as numpy optional extra; numpy added to both dev and test_extras so CI retains it.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["import dspy"] --> B["module-level: np = require('numpy')"]
    B --> C{numpy installed?}
    C -- Yes --> D["LazyLoader wraps real numpy\nadded to sys.modules"]
    C -- No --> E["_MissingModule stub\n(stores call-site frame)"]
    D --> F["First np.attr access\ntriggers real numpy import"]
    E --> G["First np.attr access\nraises ImportError with\npip install dspy[numpy] hint"]
    F --> H["Normal numpy operation"]

    subgraph "stdlib replacements (no lazy import)"
        I["math.log2 (was np.log2)"]
        J["math.inf (was np.inf)"]
        K["statistics.pstdev (was np.std)"]
        L["sum/len (was np.average)"]
    end
Loading

Reviews (14): Last reviewed commit: "docs(lazy_import): switch RST double bac..." | Re-trigger Greptile

Comment thread dspy/teleprompt/mipro_optimizer_v2.py
isaacbmiller and others added 4 commits May 8, 2026 08:38
Move numpy from required dependencies to a new [numpy] optional extra.
Add dspy/_numpy.require_numpy() helper so numpy-using features raise a
clear ImportError pointing to 'pip install dspy[numpy]' when numpy is
absent. Convert all module-level 'import numpy as np' statements to
lazy imports inside the functions that actually need numpy, so
'import dspy' continues to work when the extra is not installed.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
CI installs individual extras (uv sync --dev --extra dev, then --extra
test_extras) rather than --all-extras, so the new public [numpy] extra
alone doesn't cover test collection. Several test files import numpy
at module top, so numpy must be present in the extras CI actually
installs.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
isaacbmiller and others added 8 commits May 8, 2026 11:12
Replace dspy/utils/_numpy.require_numpy() with a generic
dspy/utils/_optional.require_optional(module, extra=...) helper modeled
on pandas.compat._optional.import_optional_dependency. dspy already
exposes other capability-gated extras (anthropic, weaviate, mcp,
langchain, optuna), so a generic helper avoids per-dep boilerplate as
new optional integrations are added.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
- mipro_optimizer_v2._set_num_trials_from_num_candidates: np.log2 -> math.log2.
- mipro_optimizer_v2._set_random_seeds: drop legacy np.random.seed call;
  MIPROv2 itself doesn't read numpy's global RNG state.
- infer_rules.compile: -np.inf -> -math.inf.

Removes the require_optional("numpy") call from both files entirely;
neither code path needs numpy now.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
require_optional previously defaulted extra=module, so a caller passing
just require_optional("foo") would advertise `pip install dspy[foo]`
even if no such extra was declared in pyproject.toml. Make extra opt-in:
when omitted, the error message only suggests `pip install <module>`.
Update the existing numpy callers to pass extra="numpy" explicitly.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
…xtra arg

Modeled on transformers' BACKENDS_MAPPING. _DSPY_EXTRAS in
dspy/utils/_optional.py maps each importable module name to its
declared install extra; require_optional(module) consults it to add
`pip install dspy[<extra>]` to the error message when an extra exists,
or falls back to `pip install <module>` otherwise. Callers no longer
write redundant extra="numpy".

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
…pt/utils

- Add dspy/utils/lazy_import.py with require(), optional(), is_available()
  and _INSTALL_HINTS registry (ported from dspy-core e8e3e19)
- Replace all require_optional() call sites with require()
- Remove dspy/utils/_optional.py
- Replace numpy with sum()/len() in teleprompt/utils.py where stdlib suffices
- Add tests for lazy_import helpers

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
@MaximeRivest

Copy link
Copy Markdown

In embedding.py and knn.py do you consider putting that at the bottom of the file instead:

# mymodule.py
def __getattr__(name):
    if name == 'np':
        globals()['np'] = require("numpy")
        return np
    raise AttributeError(f"module {__name__!r} has no attribute {name!r}")

or that in the init of the class:

# knn.py
import ...

np = None

class MyClass:
    def __init__(self):
          global np
          if np is None:
              np = require('numpy')

Comment thread dspy/predict/knn.py Outdated
Comment thread dspy/utils/lazy_import.py Outdated
Comment thread dspy/utils/lazy_import.py Outdated

@functools.cache
def is_available(module: str) -> bool:
"""Return True if ``module`` can be imported, without importing it.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

single back-tick is preferred in our settings of mkdocs.

Comment thread dspy/utils/lazy_import.py Outdated
Comment thread dspy/utils/lazy_import.py Outdated
) from e


def optional(module: str, attr: str | None = None, default: Any = None) -> Any:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

optional is not used, can we wait that it's needed before making this util?

isaacbmiller and others added 3 commits May 13, 2026 11:36
…le stub

require() now returns a LazyLoader-wrapped module (installed) or a
_MissingModule stub (missing) that raises ImportError with install hint
on first attribute access. All numpy call sites use module-level
np = require('numpy'). Removes optional() helper.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
numpy is now an optional dependency. Added install hints to API docs
for Embedder, Embeddings, KNN, KNNFewShot, SIMBA and to the RAG and
tool_use tutorials.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
…t to top of gepa.py

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
isaacbmiller and others added 2 commits May 13, 2026 11:46
- test_is_available_does_not_import_module: use a stdlib module dspy never
  imports (mailbox) instead of dspy.utils.lazy_import itself, and clean
  sys.modules via monkeypatch.delitem instead of mutating it directly.

- test_install_hints_match_pyproject_extras: replace the ad-hoc regex TOML
  parser with tomllib (tomli fallback for 3.10), and use pytestconfig.rootpath
  to locate pyproject.toml.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
…icks

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants