When a Python application starts slowly, developers often look at the main function, database connection, network calls, or configuration loading. One thing that is easy to overlook is the import system.

A large Python application can import hundreds or even thousands of modules before it actually starts doing useful work. Some of those modules may never be used during that particular execution.

Python 3.15 introduces explicit lazy imports through PEP 810. Instead of loading a module immediately, you can tell Python to load it only when the imported name is actually used.

The syntax is simple:

lazy import json

The idea is useful, but there is an important question developers should ask before changing an existing application:

How much startup time will lazy imports actually save?

That answer depends on the application. A small script may see almost no difference, while a CLI tool or large application with a deep dependency tree can benefit much more.

This article explains how lazy imports work in Python 3.15, how to measure import-related startup time, and how to decide whether using lazy imports makes sense.

What Are Lazy Imports in Python 3.15?

Normally, Python imports a module immediately.

For example:

import json

print("Application started")

When Python reaches the import json statement, it loads and executes the module before continuing.

With Python 3.15, you can explicitly make the import lazy:

lazy import json

print("Application started")

In this case, Python creates a lazy binding instead of immediately loading the module.

The module is loaded when the name is first used:

lazy import json

print("Application started")

data = json.dumps({"name": "Baibhav"})
print(data)

The json module is not needed just to reach Application started. It is loaded when json.dumps() is accessed.

This is different from simply moving the import inside a function. The lazy import stays at module level, so the dependency remains visible near the top of the file.

Why Startup Time Can Become a Problem

Importing a module is not just a matter of reading one small file.

Python may need to:

  1. Find the module.

  2. Load the module source or compiled code.

  3. Execute its top-level statements.

  4. Import its dependencies.

  5. Create classes, functions, and other objects.

  6. Run any import-time initialization.

Now imagine an application with this kind of dependency tree:

application
 ├── CLI
 ├── configuration
 ├── database
 ├── analytics
 │    ├── pandas
 │    ├── numpy
 │    └── other dependencies
 ├── reporting
 ├── image processing
 └── cloud integration

A user running:

python app.py --help

may not need analytics, image processing, or cloud integration at all.

If those modules are imported during startup, the application still pays the cost.

This is one of the situations where lazy imports can be useful.

First Measure the Import Cost

Before changing imports, measure the current application.

Python provides the -X importtime option for this purpose.

For example:

python -X importtime app.py

You can also test a particular import directly:

python -X importtime -c "import json"

The output shows the time spent importing modules, including both cumulative import time and the module's own import time.

A simplified example might look like this:

import time:      120 |      120 | json
import time:      450 |      330 | application.config
import time:     1800 |     1350 | application.analytics

The exact numbers depend on your machine and application.

The important part is not to focus on one number in isolation. Look for modules that repeatedly appear near the top of the startup cost.

Build a Baseline Before Using Lazy Imports

A good performance test starts with a baseline.

Suppose your application looks like this:

import json
import pandas
import application.analytics
import application.reporting

def main():
    print("Application started")

If main() never uses those modules during startup, you are paying for imports that may not be necessary.

First measure:

python -X importtime app.py

Then measure actual process startup separately.

For a simple application, you can use:

python -m timeit -n 10 -r 5 "import app"

For command-line applications, it is usually better to measure the actual command users run.

For example:

time python app.py --help

Run the test several times instead of relying on one result.

Startup performance can vary because of:

  • Operating system caching

  • File system state

  • CPU load

  • Background processes

  • Virtual environments

  • Disk performance

  • Python bytecode caches

The goal is to compare the same workload before and after the change.

Using Explicit Lazy Imports

Now consider an application where analytics is not needed for every command.

Instead of:

import application.analytics

you can use:

lazy import application.analytics

The rest of the application can stay the same:

lazy import application.analytics

def run_report(data):
    return application.analytics.generate_report(data)

When the application starts, the analytics module can remain unloaded.

When run_report() actually accesses it, Python loads it.

This gives you a useful middle ground:

  • The dependency stays at module level.

  • The application does not load it immediately.

  • The module is loaded automatically when required.

Lazy Imports Also Work With from Imports

You can use lazy imports with from statements as well.

For example:

lazy from application.reporting import ReportGenerator

The imported name is represented by a lazy object until it is actually accessed.

For example:

lazy from application.reporting import ReportGenerator

def create_report():
    return ReportGenerator()

The reporting module is loaded when ReportGenerator is first needed.

This can be particularly useful when a module exposes a large class or utility that is only used by one command.

A Practical CLI Example

Consider a command-line application with three commands:

myapp users
myapp reports
myapp images

The application has separate modules:

myapp/
    __main__.py
    users.py
    reports.py
    images.py

A simple implementation might import everything:

from users import run_users
from reports import run_reports
from images import run_images

The problem is that running:

python -m myapp users

may load the reporting and image-processing code even though the user only wants the users command.

Lazy imports provide another option:

lazy from users import run_users
lazy from reports import run_reports
lazy from images import run_images

The application can then load the required code as the command executes.

This is exactly the kind of workload where startup time matters because CLI applications are often short-lived processes.

Measure Before and After

Suppose the original application takes this long:

Command: myapp --help

Run 1: 0.82s
Run 2: 0.79s
Run 3: 0.81s
Run 4: 0.80s
Run 5: 0.83s

Average: 0.81s

After making several expensive imports lazy:

Run 1: 0.46s
Run 2: 0.45s
Run 3: 0.47s
Run 4: 0.44s
Run 5: 0.46s

Average: 0.46s

That is a meaningful improvement for a command that only displays help.

But do not assume that the same improvement will appear for every command.

If the reports command eventually needs all those modules, the cost has not disappeared. It has mostly moved from application startup to the point where the reporting code is first accessed.

That distinction matters.

Startup Time vs Total Execution Time

Lazy imports primarily help when the application does not need every dependency during a particular execution.

Consider:

Without lazy imports:

Startup
  ↓
Load all dependencies
  ↓
Run command

With lazy imports:

Startup
  ↓
Run command
  ↓
Load dependency when required

If a command never needs the dependency, you save the work completely.

If it does need the dependency, the work still happens.

For example:

lazy import pandas

def run():
    print("Starting...")
    data = pandas.read_csv("sales.csv")
    print("Finished")

The application can start faster, but pandas still has to load before read_csv() can execute.

So lazy imports should not be described as making every Python application faster. They mainly reduce unnecessary work during startup.

Checking Whether a Module Has Loaded

Python 3.15 also provides information that can help when investigating lazy imports.

For example:

import sys

lazy import json

print("json" in sys.modules)

json.dumps({"name": "Baibhav"})

print("json" in sys.modules)

Before the first use, the module may not yet be present in sys.modules.

After the lazy binding is accessed, the module is loaded and becomes available normally.

Python 3.15 also provides:

sys.lazy_modules

This can help with debugging and inspection of lazy imports.

For normal application code, however, you generally should not build application logic around these implementation details.

Measuring With importtime

-X importtime is especially useful when deciding where to apply lazy imports.

Start with:

python -X importtime app.py

Look for modules that:

  • Take significant time to load.

  • Pull in many dependencies.

  • Are only required by specific commands.

  • Are not needed during normal startup.

  • Are used by optional features.

For example:

application.cli
application.config
application.analytics
application.image_processing
third_party.large_library

If application.analytics is responsible for a large part of startup but is only used by the report command, it becomes a good candidate for lazy loading.

Do not blindly make every import lazy.

When Lazy Imports Are a Good Fit

Lazy imports make the most sense in a few common situations.

Large Command-Line Applications

CLI tools often have multiple commands with very different dependencies.

For example:

tool users
tool database
tool reports
tool images

There is little reason to load the image-processing stack when the user only wants tool users.

Optional Features

Suppose an application supports PDF export:

lazy import pdf_export

Users who never generate PDFs do not need to pay the import cost.

Large Applications

A large application can have many modules that are only used by specific workflows.

Lazy imports can reduce the amount of code initialized during the initial startup path.

Type-Related Imports

Some imports exist primarily to support annotations.

For example:

lazy from application.models import Customer

This can be useful when the type is needed later but should not contribute to immediate startup work.

However, always test annotation behavior and tooling in the actual project before changing large groups of imports.

When You Should Be Careful

Lazy imports change the timing of execution.

That sounds small, but it can affect real applications.

Import Errors Happen Later

With a normal import:

import broken_module

an import error happens immediately.

With:

lazy import broken_module

the error can be delayed until the name is used.

That means an application may appear to start successfully and fail later when it reaches that code path.

Import-Time Side Effects

Some modules intentionally perform work when imported.

For example:

register_plugin()
register_commands()
initialize_registry()

If that module becomes lazy, those operations will also be delayed.

This can break applications that depend on import order or module registration.

Circular Dependencies

Lazy imports can sometimes change when a circular dependency becomes visible.

They should not be used as a quick fix for poorly structured dependencies.

If two modules depend heavily on each other, it is usually better to understand and simplify that relationship.

Debugging Becomes Slightly Different

A normal import failure happens near the import statement.

A lazy import failure can happen much later.

That can make the original problem less obvious if the team is not familiar with lazy imports.

Do Not Make Everything Lazy

One of the biggest mistakes would be changing every import to:

lazy import ...

just because Python 3.15 supports it.

The goal is not to make imports lazy.

The goal is to avoid unnecessary startup work.

A better process is:

Measure
   ↓
Find expensive imports
   ↓
Understand why they are loaded
   ↓
Identify imports not needed during startup
   ↓
Make selected imports lazy
   ↓
Measure again
   ↓
Test all affected commands

This keeps the optimization focused.

Python 3.15 Also Provides Global Lazy Import Controls

Python 3.15 provides advanced controls for applications that need broader behavior.

For example:

import sys

sys.set_lazy_imports("all")

This makes top-level imports potentially lazy.

There is also:

sys.set_lazy_imports("normal")

The normal mode respects explicit lazy syntax.

These global controls are more powerful, but they also have a larger impact on application behavior.

For most application code, explicit imports are easier to reason about:

lazy import expensive_module

instead of changing the behavior of the entire application.

Python also provides a lazy import filter for advanced scenarios where some modules should remain eager.

A Better Performance Testing Strategy

If you are introducing lazy imports into a production application, use a repeatable test.

Step 1: Record the Current Startup Time

Run the same command several times.

For example:

python app.py --help

Record the results.

Step 2: Profile Imports

Use:

python -X importtime app.py --help

Find the expensive parts of the import tree.

Step 3: Select Candidates

Choose imports that:

  • Are expensive.

  • Are not needed for startup.

  • Belong to optional functionality.

  • Do not rely on import-time registration.

Step 4: Change One Group at a Time

For example:

lazy import application.reports

Do not change 100 imports at once.

Step 5: Run Functional Tests

Test both the startup path and the path that eventually uses the lazy module.

Step 6: Measure Again

Compare:

Startup time
Import time
Memory usage
Command execution time

The best optimization is one that improves the user-visible path without creating problems elsewhere.

What About Memory Usage?

Startup time is not the only reason to consider lazy imports.

Loading a large module can create functions, classes, objects, caches, and other runtime state.

If a feature is never used, that memory may not provide any value during that process.

Lazy imports allow some of that work to be postponed.

However, memory improvements depend heavily on the application.

Do not claim a fixed percentage of memory savings without measuring your own workload.

The same applies to startup time.

One application may improve significantly, while another may barely change.

Python 3.15 Lazy Imports vs Inline Imports

Before Python 3.15, developers often used this pattern:

def generate_report():
    import pandas
    return pandas.DataFrame()

It works, but the import is now hidden inside the function.

With Python 3.15, you can keep the dependency visible:

lazy import pandas

def generate_report():
    return pandas.DataFrame()

For an application that can require Python 3.15, this can make the intent clearer.

The important difference is that lazy loading becomes an explicit property of the import itself.

Supporting Older Python Versions

If your project still supports versions before Python 3.15, directly writing:

lazy import pandas

will not work on those older versions.

Python 3.15 also provides __lazy_modules__ as a compatibility-oriented mechanism.

For example:

__lazy_modules__ = ["pandas"]

import pandas

On Python versions that understand this mechanism, the import can be treated as lazy. Older versions that do not support it continue to handle the normal import behavior.

Before adopting this approach across a library, test it with every Python version your project officially supports.

A Simple Decision Checklist

Before converting an import to lazy loading, ask:

Question

If Yes

Is the module expensive to import?

Good candidate

Is it not required during startup?

Good candidate

Is it used only by one command or feature?

Good candidate

Does it depend on import-time registration?

Be careful

Does the application rely on import order?

Be careful

Does the project support Python versions before 3.15?

Check compatibility

Have you measured the change?

Proceed

Have you tested the code path that uses it?

Proceed

This keeps lazy imports as a measured optimization rather than a blanket coding style.

Common Mistakes

Making Every Import Lazy

This adds complexity without necessarily improving performance.

Measuring Only One Run

A single startup measurement is not enough to establish a useful baseline.

Ignoring Import-Time Side Effects

A module may perform important registration when it is imported.

Measuring Only Startup

A command can start faster but perform worse when the lazy module is finally loaded.

Optimizing Without Profiling

Do not guess which imports are expensive.

Use -X importtime and application-level measurements first.

Treating Lazy Imports as a Replacement for Good Architecture

If an application imports hundreds of modules because its architecture is tightly coupled, lazy imports can help with symptoms, but they do not automatically solve the underlying design problem.

Final Thoughts on Measuring Lazy Imports

Python 3.15 makes lazy imports much easier to adopt because the behavior is explicit:

lazy import module_name

That simplicity is useful, but the real value comes from applying the feature selectively.

Start by measuring your application. Find the imports that contribute to startup time. Identify which ones are not needed for the initial execution path. Make only those imports lazy, then measure the result again.

For a CLI tool with several optional features, the difference can be noticeable. For a long-running service that eventually loads most of its dependencies anyway, the benefit may be smaller.

The important thing is to measure the workload that matters to your users.

Summary

Python 3.15 introduces explicit lazy imports through the lazy keyword. Instead of loading a module immediately, Python can wait until the imported name is actually used.

This can help large applications and command-line tools that load many dependencies during startup but only use a small part of them for a particular command.

The best way to use lazy imports is not to change every import. First measure startup time with tools such as -X importtime, find the expensive imports, and then make only suitable imports lazy.

Also test for delayed import errors, import-time side effects, circular dependencies, and compatibility with older Python versions.

In short, lazy imports are a useful performance tool, but they work best when backed by real measurements rather than assumptions.