Python @ HRT At Hudson River Trading (HRT), we’ve found that centralizing our codebase facilitates cross-team collaboration and rapid deployment of new projects. Therefore, the majority of our software development takes place in a monorepo, and our Python ecosystem is set up such that internal modules are importable everywhere. Unfortunately, the convenience of this arrangement has led to a conundrum: a vast proliferation of imports. In Python, imports occur at runtime. For each imported name, the interpreter must find, load, and evaluate the contents of a corresponding module. This process gets dramatically slower for large modules, modules on distributed file systems, modules with slow side-effects (code that runs during evaluation), modules with many transitive imports, and C/C++ extension modules with many library dependencies. Most of our internal modules fall into one or more of these categories. Thus, as the sheer number of imports has increased, so too has their cumulative runtime overhead. For users, this has surfaced as scripts starting tens of seconds later, notebooks taking minutes to load, and even the simplest distributed jobs spending a substantial portion of their runtime on imports. And yet, most of our scripts and modules only refer to a few of the names defined by the modules they import. Applied recursively, this suggests that only a fraction of our imports are actually used at runtime. Is there some way we can avoid paying the cost of everything else? Lazy Imports Lazy imports are a feature we borrowed from Cinder, Meta’s performance-oriented fork of CPython. The idea is to defer the resolution and evaluation of imported modules until they’re referenced, entirely bypassing imports that are never actually used at runtime. Cinder’s implementation of lazy imports relies on two core modifications: 1. Instead of immediately executing import statements, the interpreter assigns each imported name to a placeholder LazyImport object. This object persists everything needed to resolve and evaluate the import later on, i.e. the requisite module name and an optional attribute accessed by statements of the form from module import attribute . 2. When a LazyImport is retrieved from a dict (e.g. a name is referenced, incurring a globals() lookup) the interpreter completes the resolution and evaluation, transparently returning whatever was originally imported. For example: Python from foo import bar # globals() now contains {"bar": LazyImport("foo", "bar")}. # the foo module has not yet been resolved or evaluated qux = bar() # in the evaluation of the above statement, bar is retrieved # from the globals() dict, causing foo to be imported and bar # to be reassigned to foo.bar from foo import bar # globals() now contains {"bar": LazyImport("foo", "bar")}. # the foo module has not yet been resolved or evaluated qux = bar () # in the evaluation of the above statement, bar is retrieved # from the globals() dict, causing foo to be imported and bar # to be reassigned to foo.bar Of note, imports inside try / except and with blocks are never imported lazily. Both structures commonly handle import-related errors: for example, when detecting available or compatible modules. Therefore, exceptions from import statements inside these clauses are propagated immediately, rather than when they’re referenced, so exceptions can be appropriately handled. Caveats This implementation of lazy imports is not without its quirks. For example, optimizations around the storage of function locals make it too complicated to support lazy imports in functions. There’s also no way to make imports of the form from module import * lazy, as the names exported by a module cannot be determined until evaluation. More trivially, missing modules (and similarly, typos in import statements) are not apparent until they’re referenced. Another challenge presented by lazy imports is dealing with import side-effects. Some modules make externally-visible modifications when they’re evaluated. Examples include setting sys.excepthook , updating the root logging config, and adding copyreg callbacks. With lazy imports, these side-effects are invoked when the module is first referenced rather than when it’s imported, invalidating previously safe assumptions about global state. While imports of such modules can be made eager with a try / except or with block, it’s much less convenient to apply such changes to third-party packages. To mitigate this, we also ported Cinder’s lazy exclusion mechanism: a list of module names within which imports are always evaluated eagerly. We add all our third-party packages to this list (along with a couple internal packages that rely on the aforementioned patterns). Migration A small, volunteer team of HRTers developed the initial prototype of lazy imports in Q1 2023 as part of a Surge—our annual, week-long, internal hackathon. In one week, they forked CPython 3.10, cherry-picked a handful of relevant commits from Cinder, and applied a few last fixes by hand to get everything working. Their presentation to the firm piqued widespread interest, and the project was greenlit for full-time commitment from Python@HRT (our dedicated, all-things-Python team). Over the following two quarters, we refined our implementation and began migrating the repository, starting with teams who were particularly excited to make the switch. Most cited slow script startup times or were already investigating runtime savings from manually inlining imports. The migration consisted of updating shebangs, refactoring test invocations, and fixing incompatibilities with lazy imports that generally fell into one of three categories: 1. Late or missing import side-effects. As we’ve established, lazy imports can cause import side-effects to be deferred or skipped entirely. Patterns we relied on in certain parts of the codebase, e.g. decorator registration, __init_subclass__ hooks, and global overrides were no longer guaranteed to execute at startup—or at all. Debugging these incompatibilities was the most difficult part of the migration, as the result was often a silent change in behavior or a completely unrelated exception. Fortunately, the fix was usually as simple as wrapping the offending imports in a try / except or with block. 2. Import ordering. If multiple modules have conflicting side-effects, importers implicitly depend on the order in which those modules are evaluated. For example, in our codebase, some applications unintentionally needed a particular sys.excepthook to take precedence, while others accidentally required a precise sequence of logging configurations to produce the expected output. With lazy imports, no particular import ordering is guaranteed, so much of the migration was spent specifying these dependencies through try / except blocks, lazy exclusion, and explicit side-effect invocation where possible. 3. Implicit transitive imports. Regardless of how a submodule is imported, it’s always assigned as an attribute of its parent. As a result, programs can come to depend on submodules of their imports without directly importing them. For example, suppose I import packages foo and bar , and suppose that internally, foo imports bar.baz . Normally, bar.baz will automatically be imported and accessible in my code, even though I never imported it myself. With lazy imports, the bar.baz will only be accessible if foo is referenced, which isn’t guaranteed. Fixing this class of issues was much simpler, as the resulting ImportErrors were easy to spot and address. By Q2 2024, everyone independently interested in lazy imports had made the switch, bringing us to roughly 50% lazy compatibility across the monorepo. Around the same time, we started planning our upgrade to Python 3.12. We decided it would be minimally disruptive to make our 3.12 runtime lazy by default, finishing the lazy migration alongside the version upgrade rather than migrating the remainder of the firm twice. By Q3 2024, we had ported our implementation to CPython 3.12, and as of Q2 2025, we are lazy by default across the firm! The Benefits of Laziness Lazy imports have positively affected nearly all Python users at the firm. We attribute this to three key improvements. For command line tools, lazy imports have dramatically improved the “time to do something useful”, e.g. set up the environment, validate options, or even just show --help . For tools built around our largest internal packages, imports alone could cost users several minutes, severely disrupting their work. This could be especially painful if the script exited with an error shortly after starting, requiring yet another few minutes of waiting. By minimizing startup overhead, we tightened the feedback loop for CLIs across the board, helping researchers and engineers stay focused on sequential tasks. We observed similar benefits in Jupyter notebook workflows. HRT researchers often pick and choose functionality from a wide array of internal libraries. To make this process more convenient, they will often start interactive sessions by importing everything they might want to use in advance. Unfortunately, in certain side-effect heavy contexts, this can take 10 to 15 minutes. Eliminating this overhead by skipping or deferring imports has greatly reduced the friction of starting (or restarting) notebooks. Lastly, we found that lazy imports significantly improved the efficiency of our distributed compute framework. Many of our workloads involve coordinating large batches of computationally-intensive (but relatively short-lived) jobs. Distributed work is carefully prepared by the main application, typically amounting to a parameterized function. Consequently, each job tends to use very few of its application’s imports—maybe just a couple numeric libraries and a small subset of team-specific modules. Switching to lazy imports has eliminated time spent loading unused dependencies, which in extreme cases could take longer than the job itself! The __future__ HRT has fully committed to lazy imports. Despite the maintenance and debugging costs associated with maintaining our own fork of the interpreter, we feel strongly that this feature has tangibly improved our Python ecosystem.