BioSimSpace uses the third-party lazy_import package to defer loading of its submodules (_SireWrappers, Process, etc.) until first use. It works by registering a placeholder stub module directly into sys.modules, then swapping in the real module content via importlib.reload() the first time an attribute is accessed on that stub.
This creates a race: two independent code paths (e.g. Python's/pickle's own direct sys.modules resolution vs. lazy_import's own attribute-triggered reload) can each believe they're the first to load the same dotted module name, each executing the module body once and producing its own distinct class object for the same name. The result is isinstance() checks and pickling that fail unpredictably depending on import order or execution context, since the object's "real" class no longer has a single stable identity. This is largely invisible in a simple, single-threaded script with one deterministic import order, but surfaces in multiprocessing (worker processes each deriving their own import order independently) and in cases where an optional dependency is imported before vs. after BioSimSpace.
This is the same underlying failure family as #487 (OpenFF-NAGL detection depending on import order) and #499 (isinstance(obj, Process) failing for derived classes), not isolated bugs, but recurring symptoms of the same root cause. Setting BSS_NO_LAZY_IMPORT=1 avoids it by forcing eager imports, confirming lazy loading is the trigger.
Sire's feature_lgpl branch has an in-house importlib-based replacement for the third-party lazy_import package: rather than reloading in place, it pops the stub from sys.modules before building the real module via importlib.util.find_spec/module_from_spec/exec_module, so there's no persistent stub object left for a second code path to race against. It would be good to pull this out as a separate feature and bring across to BioSimSpace too.
BioSimSpace uses the third-party lazy_import package to defer loading of its submodules (_SireWrappers, Process, etc.) until first use. It works by registering a placeholder stub module directly into sys.modules, then swapping in the real module content via importlib.reload() the first time an attribute is accessed on that stub.
This creates a race: two independent code paths (e.g. Python's/pickle's own direct sys.modules resolution vs. lazy_import's own attribute-triggered reload) can each believe they're the first to load the same dotted module name, each executing the module body once and producing its own distinct class object for the same name. The result is isinstance() checks and pickling that fail unpredictably depending on import order or execution context, since the object's "real" class no longer has a single stable identity. This is largely invisible in a simple, single-threaded script with one deterministic import order, but surfaces in multiprocessing (worker processes each deriving their own import order independently) and in cases where an optional dependency is imported before vs. after BioSimSpace.
This is the same underlying failure family as #487 (OpenFF-NAGL detection depending on import order) and #499 (isinstance(obj, Process) failing for derived classes), not isolated bugs, but recurring symptoms of the same root cause. Setting BSS_NO_LAZY_IMPORT=1 avoids it by forcing eager imports, confirming lazy loading is the trigger.
Sire's feature_lgpl branch has an in-house importlib-based replacement for the third-party lazy_import package: rather than reloading in place, it pops the stub from sys.modules before building the real module via importlib.util.find_spec/module_from_spec/exec_module, so there's no persistent stub object left for a second code path to race against. It would be good to pull this out as a separate feature and bring across to BioSimSpace too.