Python import cost on ARM64 Windows: matplotlib.pyplot took 963.8 ms
In this article
Importing matplotlib.pyplot and immediately exiting took 963.8 ms; pandas took 829.5 ms.
On the night of 2026-06-21, I measured short Python commands on an ARM64 Surface Pro. The most noticeable cost was not the work after startup but the import statement itself. Python was 3.12.10 and the machine architecture was ARM64. A plain json import took 68.3 ms, while matplotlib.pyplot reached 14.1 times that and pandas reached 12.1 times that.
CLI tools often start, parse arguments, touch a few files, and exit. Loading every external package at the entry point because it is convenient adds waiting before the actual work begins. A path that does not draw a graph or open a table still pays the same cost. Putting import pandas as pd at the top of a small helper command means waiting 829.5 ms without doing any calculation. In the small commands I use, that shows up directly in the experience.
For a one-off command this is not a problem. When I repeatedly run a checking command, the same small pause appears each time, and the entry-point cost is more memorable than the body. When a light operation needs to feel faster, I now question import placement before the algorithm.
Measuring a process that only imports
Each target was a Python process that imported one specified module. It did not calculate anything or read files inside the module. The measurement was also a prompt to reconsider putting heavy imports at the CLI entry point. I kept three wall-clock runs from process start through import completion and exit.
python -c "import matplotlib.pyplot"
python -c "import pandas"
python -c "import json"
This includes Python process startup as well as the import itself. That is a limitation, but it was part of what I wanted to observe. A real CLI user waits from pressing Enter until the prompt returns, not only for the internal import tree. I plan to fix the parser and repeat the -X importtime measurements next.
I measured standard-library modules under the same conditions. Without that baseline, it would be easy to exaggerate the package import cost. If json is 68.3 ms, sqlite3 is 68.0 ms, re is 74.2 ms, and hashlib is 44.2 ms, the lower bound for this environment is roughly that band. hashlib being faster than json is mildly interesting, but with only three runs I treated it as measurement variation.
Heavy modules were in a separate tier
Here are the medians and all three measurements for 13 modules. As described below, every cumulative_us value is 0 and the measurement column is considered a failure.
| Module | process_ms_median | runs_ms | cumulative_us |
|---|---|---|---|
| matplotlib.pyplot | 963.8ms | 834.1 / 963.8 / 998.9ms | 0 |
| pandas | 829.5ms | 844.6 / 829.5 / 794.3ms | 0 |
| numpy | 338.2ms | 246.9 / 338.2 / 381.6ms | 0 |
| requests | 304.3ms | 344.0 / 288.7 / 304.3ms | 0 |
| jinja2 | 158.5ms | 172.5 / 146.5 / 158.5ms | 0 |
| PIL.Image | 131.3ms | 182.0 / 131.3 / 126.2ms | 0 |
| markdown | 126.7ms | 126.7 / 152.9 / 113.9ms | 0 |
| yaml | 122.1ms | 122.1 / 114.5 / 123.9ms | 0 |
| lxml.etree | 107.2ms | 117.0 / 89.8 / 107.2ms | 0 |
| re | 74.2ms | 74.2 / 53.5 / 114.6ms | 0 |
| json | 68.3ms | 68.3 / 95.0 / 59.1ms | 0 |
| sqlite3 | 68.0ms | 128.7 / 68.0 / 55.8ms | 0 |
| hashlib | 44.2ms | 58.7 / 42.8 / 44.2ms | 0 |
matplotlib.pyplot and pandas clearly formed their own tier. numpy and requests followed in the 300 ms range, while jinja2, PIL.Image, markdown, yaml, and lxml.etree were roughly in the 100–160 ms band.
requests at 304.3 ms mattered in practice. A short checking command can spend that time importing requests before it makes its network call, even if the actual request is quick. Network latency will hide the difference in a long operation, but a command that reads cached data and exits immediately makes it visible.
The 963.8 ms for matplotlib.pyplot is stronger still. A tool that needs pyplot for a graph subcommand can make help and version commands pay almost a second if it imports pyplot unconditionally. I do not have evidence that this is special to ARM64 Windows, but it was hard to ignore in my environment.
The importtime column failed
I initially planned to parse python -X importtime output and record per-module cumulative time as cumulative_us. That should have shown which part of the internal tree was waiting.
Instead, the resulting JSON had cumulative_us equal to 0 for everything: json, pandas, and matplotlib.pyplot, all 13 entries. The column cannot be used as a measurement.
The cause was a parser bug I wrote. Without checking the actual -X importtime format, I assumed that line.split("|")[1] would return the second column. The output did not have that shape, so the parser failed to extract a value and returned 0 through its exception path (the failure was quiet enough that I briefly wondered whether the values really were zero).
This means the article can speak only about total process wall time, not the internal import tree. It would be dangerous to pretend to know which internal module makes matplotlib or pandas wait. The measurement showed the entry-point pause, but not its structure. The failure shifted the article from import-tree analysis toward the startup time a CLI user actually experiences.
Imports matter most for short CLIs
For a small Python command, import placement becomes response time. This measurement was close to the CLI experience, which is why I found it useful. If a command only reads a configuration file, yaml at 122.1 ms is often acceptable. If it emits one template, jinja2 at 158.5 ms is not necessarily too large for one invocation.
Repeated helper commands accumulate the cost. Starting pandas ten times means 8,295.0 ms spent only on import; matplotlib.pyplot means 9,638.0 ms. A single invocation may be tolerable, but repeated build scripts or monitoring tasks turn it into a persistent feeling of slowness.
The standard-library results were reassuring: json, re, sqlite3, and hashlib stayed between 44.2 ms and 74.2 ms, close to Python startup itself. Keeping a short CLI within the standard library can keep startup waiting small. I therefore started moving only the imports whose cost was visible into the code paths that need them.
Move only the slow imports inward
The simple fix is to move a slow import inside the function that needs it: a lazy import.
def plot_report(path):
import matplotlib.pyplot as plt
...
With this structure, help, configuration checks, and light JSON output do not need to load matplotlib.pyplot; the command pays 963.8 ms only when it draws a graph. The same applies to pandas: import it when entering the branch that processes a CSV as a DataFrame.
Moving every import into a function does not automatically improve readability. Pushing frequently used standard-library modules inward would make the code harder to follow. A practical boundary was whether an external package took more than 100 ms at the CLI entry point; I did not want to optimize away json at 68.3 ms or hashlib at 44.2 ms.
The next measurement should use a corrected -X importtime parser and recover the internal cumulative times. In this run, every cumulative_us value was 0, so that part completely failed. Still, total process time was enough to show the cost of import statements in short Python CLIs.