Skip to content

add stub-driven lazy imports using lazy_loader - #3459

Open
deruyter92 wants to merge 122 commits into
devfrom
jaap/lazy-loading
Open

deruyter92 wants to merge 122 commits into
devfrom
jaap/lazy-loading

Conversation

@deruyter92

@deruyter92 deruyter92 commented Aug 21, 2026 •

Copy link
Copy Markdown
Collaborator

Summary
Replace DeepLabCut's hand-maintained lazy export maps (_API_EXPORTS_MAP, _OPTIONAL_EXPORTS, and the all export lists) with Scientific Python's lazy_loader.attach_stub.

deeplabcut/__init__.pyi is now the source of truth for public API. Type checkers can use use it, and at runtime
lazy_loader parses it to install __getattr__, __dir__, and __all__, as well.

Each implementation module is only imported when its attribute is first accessed. This avoids eagerly importing heavy or optional modules such as pose_estimation_pytorch, gui, and pose_tracking_pytorch during import deeplabcut.

Note that this shifts the import-time drastically to lower-level heavy modules
import_times

Changes

  • add lazy_loader as a runtime dependency,
  • add __init__.pyi and py.typed
  • add a type-checking tool that checks the stubs in CI see 6b3c8e5
  • add runtime tests that check if all exports can be resolved
  • add GUI warm-up to eagerly load modules at launch (see 411237)
  • fix the star-all imports in deeplabcut.utils
  • move the DEBUG constant to it's own module
  • Behavior change: for exports whose dependencies are not installed, attribute access now raises ModuleNotFoundError instead of AttributeError which is also propagated through hassattr

Note

For future reference: this PR is scoped to change only the import strategy while fixing some big issues like circular imports. During the work, I found that within the package a lot of import hygiene can still be applied at lower levels. For instance: many modules import heavy dependencies in core, such as pandas, numpy or numba. Fixing these would yield another big win.

Comment thread deeplabcut/__init__.py Outdated
Comment thread deeplabcut/__init__.py Outdated

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Will this ever run in CI or is it meant as local only?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

A few notes:

  • this fixture is now moved to tools instead of tests, together with the actual tool test_type_stub.py that uses it. This enables running it as a separate tool without deeplabcut or any dependencies installed.
  • it is now hooked into CI via a separate lightweight type checking test see 6b3c8e5

There are also some tests that check if the imports resolve at runtime defined in test_top_level_api.py. See other comment below. Let me know if you think this is excessive.

Comment thread tests/test_top_level_api.py Outdated


@pytest.mark.skipif(
not (_module_available("torch") and _module_available("PySide6")),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The skip condition does not represent the full optional dependency set, right? With PySide6 installed but missing another GUI dep, this would run. Can we mark this test in the CI test selector maybe? Or maybe there are other minimal solutions that don't require copying the deps from pyproject at all

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I am reading the GUI deps from pyproject.toml now and removed the skipif everything runs now, and those modules that require GUI dependencies are excepted in case these dependencies are missing.

Note that we now have two runtime tests that resolve all exports (+ a type-checking tool that can run in CI to test the stubs).

let me know if you think this is excessive

Comment thread deeplabcut/__init__.py Outdated
Comment thread deeplabcut/__init__.pyi
@deruyter92
deruyter92 changed the base branch from jaap/prepare_tf_deprecation to jaap/refresh-deprecation-markers August 28, 2026 07:53
@deruyter92 deruyter92 added the 3.1 label Aug 28, 2026
@C-Achard

C-Achard commented Aug 31, 2026 •

Copy link
Copy Markdown
Collaborator

Note: In deeplabcut\gui\utils.py::is_latest_deeplabcut_version, as well as window.py, we may want to change the import to deeplabcut.version instead of top-level deeplabcut. Or is that not a concern generally?

@C-Achard

C-Achard commented Aug 31, 2026 •

Copy link
Copy Markdown
Collaborator

Also the import all calls in .utils.__init__ should change, I think it's a good PR to do this; happy to make another PR on top of this one also

@C-Achard

Copy link
Copy Markdown
Collaborator

For the GUI, could we disable lazy imports ? Having the interface freeze on imports makes it seem unresponsive, in this case the previous behavior of importing all on launch may be a bit more user-friendly

@deruyter92

Copy link
Copy Markdown
Collaborator Author

@C-Achard, thanks for your review comments.

Note: In deeplabcut\gui\utils.py::is_latest_deeplabcut_version, as well as window.py, we may want to change the import to deeplabcut.version instead of top-level deeplabcut. Or is that not a concern generally?

With the current implementation, deeplabcut.version is eagerly bound at __init__.py, so this is not a concern.

Also the import all calls in .utils.init should change, I think it's a good PR to do this; happy to make another PR on top of this one also

Done, thanks

For the GUI, could we disable lazy imports ? Having the interface freeze on imports makes it seem unresponsive, in this case the previous behavior of importing all on launch may be a bit more user-friendly

I've added a GUI warm-up now, that imports all public deeplabcut exports + heavy imports on a deamon thread.

@deruyter92
deruyter92 marked this pull request as ready for review September 3, 2026 13:54
@deruyter92
deruyter92 requested a review from C-Achard September 3, 2026 13:54
@deruyter92
deruyter92 changed the base branch from jaap/refresh-deprecation-markers to dev September 3, 2026 15:21
@deruyter92
deruyter92 marked this pull request as draft September 3, 2026 15:21
@deruyter92
deruyter92 marked this pull request as ready for review September 3, 2026 15:21
deruyter92 and others added 23 commits September 4, 2026 09:01
…kers

update deprecation marker versions (TF deprecation since 3.1, removed in 3.2)
Replace DeepLabCut's hand-maintained lazy export maps
(_API_EXPORTS_MAP, _OPTIONAL_EXPORTS, and the __all__ export lists) with
Scientific Python's `lazy_loader.attach_stub`.

`deeplabcut/__init__.pyi` is
now the single declarative source of truth for the public API: static
tools read it for discoverability and signatures, while lazy_loader
parses it at runtime to install `__getattr__`, `__dir__`, and `__all__`,
importing each implementation module only when its attribute is first
accessed.

This keeps the flat API intact and
avoids eagerly importing heavy or optional modules such as
`pose_estimation_pytorch`, `gui`, and `pose_tracking_pytorch` during
`import deeplabcut`.

- add `lazy_loader` as a runtime dependency,
- add `__init__.pyi` and
`py.typed`
- add tests for the new loading behavior.
deeplabcut.api is the canonical entrypoint and its modules are cheap.
Eager re-exports make deeplabcut.api.<name> an ordinary attribute that type checkers resolve without a stub.
…bcut.utils

Currently deplabcut.utils is advertised as public entrypoint (e.g. see deeblabcut.__all__).

We might want to drop this as API entrypoint in a separate PR
…azy attributes.

The error handling was introduced to provide actionable messages when a specific (extra) dependency was missing, but:
- torch is not an extra dependency, so should be installed and not have exceptional error messaging
- the gui components are normally never imported from external scripts, only launched. raw ModuleNotFound errors + a stack trace are clear enough and don't warrant any special casing.
it was exported but missing from `__all__`. This commit ensures parity with the state before lazy loading was introduced.
`DEBUG` was missing from __all__, but included in the exports, but now it is treated like any other exported symbol
Also: do not skip the subprocess resolve all API for non-GUI modules. (i.e. only skip GUI-requiring modules if not installed)
`deeplabcut/utils/__init__.py` previously star-imported eight submodules, so many symbols were reachable directly  as `from deeplabcut.utils import X`.
Replacing that with lazy_loader.attach dropped them all.

This commit restores the symbols manually, resolving lazily. Third party dependencies and accidental leaks are not restored (numpy, os, logger, …).

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

3.1 enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants