Skip to content

Commit 4f69094

Browse files
core[performance]: use custom __getattr__ in __init__.py files for lazy imports (#30769)
Most easily reviewed with the "hide whitespace" option toggled. Seeing 10-50% speed ups in import time for common structures 🚀 The general purpose of this PR is to lazily import structures within `langchain_core.XXX_module.__init__.py` so that we're not eagerly importing expensive dependencies (`pydantic`, `requests`, etc). Analysis of flamegraphs generated with `importtime` motivated these changes. For example, the one below demonstrates that importing `HumanMessage` accidentally triggered imports for `importlib.metadata`, `requests`, etc. There's still much more to do on this front, and we can start digging into our own internal code for optimizations now that we're less concerned about external imports. <img width="1210" alt="Screenshot 2025-04-11 at 1 10 54 PM" src="https://github.com/user-attachments/assets/112a3fe7-24a9-4294-92c1-d5ae64df839e" /> I've tracked the improvements with some local benchmarks: ## `pytest-benchmark` results | Name | Before (s) | After (s) | Delta (s) | % Change | |-----------------------------|------------|-----------|-----------|----------| | Document | 2.8683 | 1.2775 | -1.5908 | -55.46% | | HumanMessage | 2.2358 | 1.1673 | -1.0685 | -47.79% | | ChatPromptTemplate | 5.5235 | 2.9709 | -2.5526 | -46.22% | | Runnable | 2.9423 | 1.7793 | -1.163 | -39.53% | | InMemoryVectorStore | 3.1180 | 1.8417 | -1.2763 | -40.93% | | RunnableLambda | 2.7385 | 1.8745 | -0.864 | -31.55% | | tool | 5.1231 | 4.0771 | -1.046 | -20.42% | | CallbackManager | 4.2263 | 3.4099 | -0.8164 | -19.32% | | LangChainTracer | 3.8394 | 3.3101 | -0.5293 | -13.79% | | BaseChatModel | 4.3317 | 3.8806 | -0.4511 | -10.41% | | PydanticOutputParser | 3.2036 | 3.2995 | 0.0959 | 2.99% | | InMemoryRateLimiter | 0.5311 | 0.5995 | 0.0684 | 12.88% | Note the lack of change for `InMemoryRateLimiter` and `PydanticOutputParser` is just random noise, I'm getting comparable numbers locally. ## Local CodSpeed results We're still working on configuring CodSpeed on CI. The local usage produced similar results.
1 parent ada740b commit 4f69094

File tree

17 files changed

+949
-292
lines changed

17 files changed

+949
-292
lines changed

libs/core/langchain_core/_api/__init__.py

Lines changed: 48 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,20 +9,24 @@
99
1010
"""
1111

12-
from .beta_decorator import (
13-
LangChainBetaWarning,
14-
beta,
15-
suppress_langchain_beta_warning,
16-
surface_langchain_beta_warnings,
17-
)
18-
from .deprecation import (
19-
LangChainDeprecationWarning,
20-
deprecated,
21-
suppress_langchain_deprecation_warning,
22-
surface_langchain_deprecation_warnings,
23-
warn_deprecated,
24-
)
25-
from .path import as_import_path, get_relative_path
12+
from importlib import import_module
13+
from typing import TYPE_CHECKING
14+
15+
if TYPE_CHECKING:
16+
from .beta_decorator import (
17+
LangChainBetaWarning,
18+
beta,
19+
suppress_langchain_beta_warning,
20+
surface_langchain_beta_warnings,
21+
)
22+
from .deprecation import (
23+
LangChainDeprecationWarning,
24+
deprecated,
25+
suppress_langchain_deprecation_warning,
26+
surface_langchain_deprecation_warnings,
27+
warn_deprecated,
28+
)
29+
from .path import as_import_path, get_relative_path
2630

2731
__all__ = [
2832
"as_import_path",
@@ -37,3 +41,33 @@
3741
"surface_langchain_deprecation_warnings",
3842
"warn_deprecated",
3943
]
44+
45+
_dynamic_imports = {
46+
"LangChainBetaWarning": "beta_decorator",
47+
"beta": "beta_decorator",
48+
"suppress_langchain_beta_warning": "beta_decorator",
49+
"surface_langchain_beta_warnings": "beta_decorator",
50+
"as_import_path": "path",
51+
"get_relative_path": "path",
52+
"LangChainDeprecationWarning": "deprecation",
53+
"deprecated": "deprecation",
54+
"surface_langchain_deprecation_warnings": "deprecation",
55+
"suppress_langchain_deprecation_warning": "deprecation",
56+
"warn_deprecated": "deprecation",
57+
}
58+
59+
60+
def __getattr__(attr_name: str) -> object:
61+
module_name = _dynamic_imports.get(attr_name)
62+
package = __spec__.parent # type: ignore[name-defined]
63+
if module_name == "__module__" or module_name is None:
64+
result = import_module(f".{attr_name}", package=package)
65+
else:
66+
module = import_module(f".{module_name}", package=package)
67+
result = getattr(module, attr_name)
68+
globals()[attr_name] = result
69+
return result
70+
71+
72+
def __dir__() -> list[str]:
73+
return list(__all__)

libs/core/langchain_core/callbacks/__init__.py

Lines changed: 97 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -7,46 +7,50 @@
77
BaseCallbackHandler --> <name>CallbackHandler # Example: AimCallbackHandler
88
"""
99

10-
from langchain_core.callbacks.base import (
11-
AsyncCallbackHandler,
12-
BaseCallbackHandler,
13-
BaseCallbackManager,
14-
CallbackManagerMixin,
15-
Callbacks,
16-
ChainManagerMixin,
17-
LLMManagerMixin,
18-
RetrieverManagerMixin,
19-
RunManagerMixin,
20-
ToolManagerMixin,
21-
)
22-
from langchain_core.callbacks.file import FileCallbackHandler
23-
from langchain_core.callbacks.manager import (
24-
AsyncCallbackManager,
25-
AsyncCallbackManagerForChainGroup,
26-
AsyncCallbackManagerForChainRun,
27-
AsyncCallbackManagerForLLMRun,
28-
AsyncCallbackManagerForRetrieverRun,
29-
AsyncCallbackManagerForToolRun,
30-
AsyncParentRunManager,
31-
AsyncRunManager,
32-
BaseRunManager,
33-
CallbackManager,
34-
CallbackManagerForChainGroup,
35-
CallbackManagerForChainRun,
36-
CallbackManagerForLLMRun,
37-
CallbackManagerForRetrieverRun,
38-
CallbackManagerForToolRun,
39-
ParentRunManager,
40-
RunManager,
41-
adispatch_custom_event,
42-
dispatch_custom_event,
43-
)
44-
from langchain_core.callbacks.stdout import StdOutCallbackHandler
45-
from langchain_core.callbacks.streaming_stdout import StreamingStdOutCallbackHandler
46-
from langchain_core.callbacks.usage import (
47-
UsageMetadataCallbackHandler,
48-
get_usage_metadata_callback,
49-
)
10+
from importlib import import_module
11+
from typing import TYPE_CHECKING
12+
13+
if TYPE_CHECKING:
14+
from langchain_core.callbacks.base import (
15+
AsyncCallbackHandler,
16+
BaseCallbackHandler,
17+
BaseCallbackManager,
18+
CallbackManagerMixin,
19+
Callbacks,
20+
ChainManagerMixin,
21+
LLMManagerMixin,
22+
RetrieverManagerMixin,
23+
RunManagerMixin,
24+
ToolManagerMixin,
25+
)
26+
from langchain_core.callbacks.file import FileCallbackHandler
27+
from langchain_core.callbacks.manager import (
28+
AsyncCallbackManager,
29+
AsyncCallbackManagerForChainGroup,
30+
AsyncCallbackManagerForChainRun,
31+
AsyncCallbackManagerForLLMRun,
32+
AsyncCallbackManagerForRetrieverRun,
33+
AsyncCallbackManagerForToolRun,
34+
AsyncParentRunManager,
35+
AsyncRunManager,
36+
BaseRunManager,
37+
CallbackManager,
38+
CallbackManagerForChainGroup,
39+
CallbackManagerForChainRun,
40+
CallbackManagerForLLMRun,
41+
CallbackManagerForRetrieverRun,
42+
CallbackManagerForToolRun,
43+
ParentRunManager,
44+
RunManager,
45+
adispatch_custom_event,
46+
dispatch_custom_event,
47+
)
48+
from langchain_core.callbacks.stdout import StdOutCallbackHandler
49+
from langchain_core.callbacks.streaming_stdout import StreamingStdOutCallbackHandler
50+
from langchain_core.callbacks.usage import (
51+
UsageMetadataCallbackHandler,
52+
get_usage_metadata_callback,
53+
)
5054

5155
__all__ = [
5256
"dispatch_custom_event",
@@ -84,3 +88,56 @@
8488
"UsageMetadataCallbackHandler",
8589
"get_usage_metadata_callback",
8690
]
91+
92+
_dynamic_imports = {
93+
"AsyncCallbackHandler": "base",
94+
"BaseCallbackHandler": "base",
95+
"BaseCallbackManager": "base",
96+
"CallbackManagerMixin": "base",
97+
"Callbacks": "base",
98+
"ChainManagerMixin": "base",
99+
"LLMManagerMixin": "base",
100+
"RetrieverManagerMixin": "base",
101+
"RunManagerMixin": "base",
102+
"ToolManagerMixin": "base",
103+
"FileCallbackHandler": "file",
104+
"AsyncCallbackManager": "manager",
105+
"AsyncCallbackManagerForChainGroup": "manager",
106+
"AsyncCallbackManagerForChainRun": "manager",
107+
"AsyncCallbackManagerForLLMRun": "manager",
108+
"AsyncCallbackManagerForRetrieverRun": "manager",
109+
"AsyncCallbackManagerForToolRun": "manager",
110+
"AsyncParentRunManager": "manager",
111+
"AsyncRunManager": "manager",
112+
"BaseRunManager": "manager",
113+
"CallbackManager": "manager",
114+
"CallbackManagerForChainGroup": "manager",
115+
"CallbackManagerForChainRun": "manager",
116+
"CallbackManagerForLLMRun": "manager",
117+
"CallbackManagerForRetrieverRun": "manager",
118+
"CallbackManagerForToolRun": "manager",
119+
"ParentRunManager": "manager",
120+
"RunManager": "manager",
121+
"adispatch_custom_event": "manager",
122+
"dispatch_custom_event": "manager",
123+
"StdOutCallbackHandler": "stdout",
124+
"StreamingStdOutCallbackHandler": "streaming_stdout",
125+
"UsageMetadataCallbackHandler": "usage",
126+
"get_usage_metadata_callback": "usage",
127+
}
128+
129+
130+
def __getattr__(attr_name: str) -> object:
131+
module_name = _dynamic_imports.get(attr_name)
132+
package = __spec__.parent # type: ignore[name-defined]
133+
if module_name == "__module__" or module_name is None:
134+
result = import_module(f".{attr_name}", package=package)
135+
else:
136+
module = import_module(f".{module_name}", package=package)
137+
result = getattr(module, attr_name)
138+
globals()[attr_name] = result
139+
return result
140+
141+
142+
def __dir__() -> list[str]:
143+
return list(__all__)
Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
"""Document loaders."""
22

3-
from langchain_core.document_loaders.base import BaseBlobParser, BaseLoader
4-
from langchain_core.document_loaders.blob_loaders import Blob, BlobLoader, PathLike
5-
from langchain_core.document_loaders.langsmith import LangSmithLoader
3+
from importlib import import_module
4+
from typing import TYPE_CHECKING
5+
6+
if TYPE_CHECKING:
7+
from langchain_core.document_loaders.base import BaseBlobParser, BaseLoader
8+
from langchain_core.document_loaders.blob_loaders import Blob, BlobLoader, PathLike
9+
from langchain_core.document_loaders.langsmith import LangSmithLoader
610

711
__all__ = [
812
"BaseBlobParser",
@@ -12,3 +16,28 @@
1216
"PathLike",
1317
"LangSmithLoader",
1418
]
19+
20+
_dynamic_imports = {
21+
"BaseBlobParser": "base",
22+
"BaseLoader": "base",
23+
"Blob": "blob_loaders",
24+
"BlobLoader": "blob_loaders",
25+
"PathLike": "blob_loaders",
26+
"LangSmithLoader": "langsmith",
27+
}
28+
29+
30+
def __getattr__(attr_name: str) -> object:
31+
module_name = _dynamic_imports.get(attr_name)
32+
package = __spec__.parent # type: ignore[name-defined]
33+
if module_name == "__module__" or module_name is None:
34+
result = import_module(f".{attr_name}", package=package)
35+
else:
36+
module = import_module(f".{module_name}", package=package)
37+
result = getattr(module, attr_name)
38+
globals()[attr_name] = result
39+
return result
40+
41+
42+
def __dir__() -> list[str]:
43+
return list(__all__)

libs/core/langchain_core/documents/__init__.py

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,34 @@
55
66
"""
77

8-
from langchain_core.documents.base import Document
9-
from langchain_core.documents.compressor import BaseDocumentCompressor
10-
from langchain_core.documents.transformers import BaseDocumentTransformer
8+
from importlib import import_module
9+
from typing import TYPE_CHECKING
10+
11+
if TYPE_CHECKING:
12+
from .base import Document
13+
from .compressor import BaseDocumentCompressor
14+
from .transformers import BaseDocumentTransformer
1115

1216
__all__ = ["Document", "BaseDocumentTransformer", "BaseDocumentCompressor"]
17+
18+
_dynamic_imports = {
19+
"Document": "base",
20+
"BaseDocumentCompressor": "compressor",
21+
"BaseDocumentTransformer": "transformers",
22+
}
23+
24+
25+
def __getattr__(attr_name: str) -> object:
26+
module_name = _dynamic_imports.get(attr_name)
27+
package = __spec__.parent # type: ignore[name-defined]
28+
if module_name == "__module__" or module_name is None:
29+
result = import_module(f".{attr_name}", package=package)
30+
else:
31+
module = import_module(f".{module_name}", package=package)
32+
result = getattr(module, attr_name)
33+
globals()[attr_name] = result
34+
return result
35+
36+
37+
def __dir__() -> list[str]:
38+
return list(__all__)
Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,35 @@
11
"""Embeddings."""
22

3-
from langchain_core.embeddings.embeddings import Embeddings
4-
from langchain_core.embeddings.fake import DeterministicFakeEmbedding, FakeEmbeddings
3+
from importlib import import_module
4+
from typing import TYPE_CHECKING
5+
6+
if TYPE_CHECKING:
7+
from langchain_core.embeddings.embeddings import Embeddings
8+
from langchain_core.embeddings.fake import (
9+
DeterministicFakeEmbedding,
10+
FakeEmbeddings,
11+
)
512

613
__all__ = ["DeterministicFakeEmbedding", "Embeddings", "FakeEmbeddings"]
14+
15+
_dynamic_imports = {
16+
"Embeddings": "embeddings",
17+
"DeterministicFakeEmbedding": "fake",
18+
"FakeEmbeddings": "fake",
19+
}
20+
21+
22+
def __getattr__(attr_name: str) -> object:
23+
module_name = _dynamic_imports.get(attr_name)
24+
package = __spec__.parent # type: ignore[name-defined]
25+
if module_name == "__module__" or module_name is None:
26+
result = import_module(f".{attr_name}", package=package)
27+
else:
28+
module = import_module(f".{module_name}", package=package)
29+
result = getattr(module, attr_name)
30+
globals()[attr_name] = result
31+
return result
32+
33+
34+
def __dir__() -> list[str]:
35+
return list(__all__)

libs/core/langchain_core/example_selectors/__init__.py

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,19 @@
44
This allows us to select examples that are most relevant to the input.
55
"""
66

7-
from langchain_core.example_selectors.base import BaseExampleSelector
8-
from langchain_core.example_selectors.length_based import (
9-
LengthBasedExampleSelector,
10-
)
11-
from langchain_core.example_selectors.semantic_similarity import (
12-
MaxMarginalRelevanceExampleSelector,
13-
SemanticSimilarityExampleSelector,
14-
sorted_values,
15-
)
7+
from importlib import import_module
8+
from typing import TYPE_CHECKING
9+
10+
if TYPE_CHECKING:
11+
from langchain_core.example_selectors.base import BaseExampleSelector
12+
from langchain_core.example_selectors.length_based import (
13+
LengthBasedExampleSelector,
14+
)
15+
from langchain_core.example_selectors.semantic_similarity import (
16+
MaxMarginalRelevanceExampleSelector,
17+
SemanticSimilarityExampleSelector,
18+
sorted_values,
19+
)
1620

1721
__all__ = [
1822
"BaseExampleSelector",
@@ -21,3 +25,27 @@
2125
"SemanticSimilarityExampleSelector",
2226
"sorted_values",
2327
]
28+
29+
_dynamic_imports = {
30+
"BaseExampleSelector": "base",
31+
"LengthBasedExampleSelector": "length_based",
32+
"MaxMarginalRelevanceExampleSelector": "semantic_similarity",
33+
"SemanticSimilarityExampleSelector": "semantic_similarity",
34+
"sorted_values": "semantic_similarity",
35+
}
36+
37+
38+
def __getattr__(attr_name: str) -> object:
39+
module_name = _dynamic_imports.get(attr_name)
40+
package = __spec__.parent # type: ignore[name-defined]
41+
if module_name == "__module__" or module_name is None:
42+
result = import_module(f".{attr_name}", package=package)
43+
else:
44+
module = import_module(f".{module_name}", package=package)
45+
result = getattr(module, attr_name)
46+
globals()[attr_name] = result
47+
return result
48+
49+
50+
def __dir__() -> list[str]:
51+
return list(__all__)

0 commit comments

Comments
 (0)