Repository navigation
perf: defer heavy imports on Python 3.15 with __lazy_modules__ - #931
Merged
Merged
Conversation
Use PEP 810 `__lazy_modules__` so `import braintrust` on Python 3.15 no longer eagerly loads jsonschema, tqdm, asyncio, slugify, or gzip. On 3.10-3.14 `__lazy_modules__` is an unused variable. Modules star-exported from `braintrust/__init__.py` without an `__all__` (logger, functions/stream, functions/invoke, framework2, audit) bind the deferred module under a private alias, since `from m import *` would otherwise reify it. framework and logger now call `os.cpu_count()` instead of `multiprocessing.cpu_count()`, which drops the multiprocessing import on all versions. As a side effect, the accidental stdlib re-exports `braintrust.asyncio`, `braintrust.gzip`, `braintrust.slugify`, and `braintrust.cpu_count` are gone on every version. Interleaved medians, 60 rounds, `python -c "import braintrust"`: - 3.15.0: 472 ms -> 357 ms (-24%), 511 -> 403 modules - 3.14.6: 472 ms -> 469 ms (noise), 502 -> 493 modules Adds a pyperf import-time benchmark and a 3.15-only test that checks sys.modules after import, plus an Eval/parameters smoke test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reuse the existing subprocess helper instead of a copy. Both tests now skip below 3.15, where they only duplicated existing coverage. The deferral test derives its module list from the `__lazy_modules__` declarations, so new entries are covered automatically. The import-time benchmark now runs a plain `import braintrust` in the child process. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
… test On CPython < 3.12 a nested import on one thread makes importlib raise a spurious `KeyError: <thread id>`. openrouter's lazy sub-SDK loader handles that by popping the module from sys.modules and re-importing it. That creates a new `Embeddings` class, and setup() patches the new one, while the test still held the class it imported at module load. The test then saw an unwrapped `Embeddings.generate`. Windows/3.10 CI started hitting this once `import braintrust` stopped pulling in `multiprocessing`, which shifts what openrouter's GC finalizer (`asyncio.run(aclose())`) imports for the first time. Look the classes up when the test runs, as setup() does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
import braintrustnow loads about 24% faster on Python 3.15. It uses PEP 810__lazy_modules__to deferjsonschema,tqdm,asyncio,slugify, andgzipuntil first use. On 3.10–3.14__lazy_modules__is an unused variable, so behavior there is unchanged apart from the two notes below.Follow-up to #924.
Changes
__lazy_modules__declarations inparameters,framework,framework2,logger,trace,workflow_eval,sandbox,audit,functions/stream,functions/invoke, andprompt_cache/disk_cache.__init__.pystar-imports five of those modules, and they have no__all__:logger,functions/stream,functions/invoke,framework2, andaudit.from m import *would reify their lazy names, so they bind the deferred module under a private alias (import asyncio as _asyncio).frameworkandloggercallos.cpu_count()instead ofmultiprocessing.cpu_count(). Both call it at import time, and the switch drops themultiprocessingimport on every version. The only difference is when the CPU count can't be determined: the import used to raise, and now the thread pools fall back to their default size.Visible on all versions
The accidental stdlib re-exports
braintrust.asyncio,braintrust.gzip,braintrust.slugify, andbraintrust.cpu_countno longer exist. Nothing in this repo uses them. Keeping them would mean adding__all__tologgerand the other star-exported modules, which changes the public API far more.Measurements
Median of 60 interleaved runs of
python -c "import braintrust"on a Linux devbox:Per step on 3.15:
jsonschema+tqdm: −15%asyncio: −21%slugify+gzip: −22% to −23%os.cpu_count(): −24%The last two steps saved about 5 ms each, close to the noise floor.
This box drifted ±10% between sequential pyperf runs, so the interleaved numbers are the primary measurement.
make bench-compareon adjacent runs agrees: 3.15 is 1.23x faster and 3.14 is 1.02x faster. Over the same runs, the unchangedpython -c passcontrol read 1.21x slower, which is pure drift.Not attempted:
requests(~100 ms):api/_transport.pysubclassesHTTPAdapterat class-definition time._generated_types: it is star-exported, so lazy imports can't help it.Tests and benchmark
benchmarks/benches/bench_import_time.py. It measurespython -c passandimport braintrustseparately, so interpreter startup can be subtracted.test_lazy_integration_imports.py:braintrust.*module's__lazy_modules__and asserts none of those modules (normultiprocessing) is insys.modulesafter import. It fails onmainand passes on this branch.Evaland checksjsonschemaparameter validation through the deferred imports.OpenRouter test fix
This PR made
test_openrouter_integration_setup_is_idempotentfail on Windows with Python 3.10 only. The root cause is old: before 3.12, a nested import on one thread can makeimportlibraise a spuriousKeyError. OpenRouter's SDK handles that by deleting and re-importing the sub-module, which creates a newEmbeddingsclass.setup()then patches the new class while the test still holds the old one. Droppingmultiprocessingfromimport braintrustlikely changes what OpenRouter's GC finalizer imports for the first time, which surfaced the problem on Windows. The test now resolves the classes when it runs, the same waysetup()does. Forcing that re-import on Linux 3.10 reproduces the exact CI failure, and the fix passes it.Validation (local, Linux)
nox -s test_core: 3.10.22 ✅ (1012 passed), 3.14.6 ✅ (1017 passed), 3.15.0 ✅ (1024 passed).pylintandtest_typeson 3.15.0 ✅. pre-commit ✅.🤖 Generated with Claude Code
Co-authored by StarfolkAI (@starfolkai)[bot]