Maintenance

Site is under maintenance — quizzes are still available.

Go to quizzes
Sponsored Reserved space — layout preview until AdSense is connected

Python

What Actually Happens When You Type `import something` in Python?

This article explores the internal mechanics of Python's import system, from searching and loading modules to handling circular imports and the role of sys.modules. It explains how imports work at runtime and offers practical tips for avoiding bugs and optimizing startup performance.

June 2026 · 8 min read · 1 views · 0 hearts

What Actually Happens When You Type import something in Python?

If you've written more than 10 lines of Python, you've used an import statement. But have you ever stopped to wonder what's actually happening under the hood? That single line import pandas sets off a surprisingly sophisticated chain of events—one that, when understood, can save you from mysterious bugs, circular import errors, and painfully slow startup times.

Let's pull back the curtain on Python's import system.

The Three-Step Dance

Every time you write import foo, Python goes through three distinct phases:

  1. Searching – Where is foo located?
  2. Loading – How do we execute the code in foo?
  3. Binding – How does the name foo become available in your namespace?

Python doesn't just check one place. It searches through a priority list called sys.path, which is assembled at runtime from: - The directory of the script you're running - The PYTHONPATH environment variable - Standard library paths and site-packages directories

Here's the kicker: the first match wins. If there's a local file named random.py in your working directory, import random will import that instead of the standard library's random module. Yes, that's bitten more than a few developers mid-debug session.

The Module Cache: sys.modules

Here's something that surprises many developers: Python only loads a module once per interpreter session.

When you import a module, Python first checks a dictionary called sys.modules. If the module is already there, it just binds the name and moves on. No re-execution. No second file read.

This is why doing from module import something after import module doesn't re-read the file. It's also why having mutable global state in modules can be a recipe for head-scratching bugs—anything you import from that module shares the same underlying object.

The Five Flavors of Import (and Why Most Are Slow)

Python's import system supports five module types, and it tries them in order:

  1. Built-in modules (like sys, math)
  2. Frozen modules (C extensions compiled into the interpreter)
  3. Python source files (.py)
  4. Compiled bytecode (.pyc)
  5. C extensions (.so, .pyd)

Each of these uses a different loader. The source file loader has to read, tokenize, parse, and compile your code into bytecode before executing it. That's why .pyc files exist—they save the compilation step on subsequent runs.

Pro tip: If you're importing a large module for the first time and startup speed matters, pre-compiling with compileall can shave off milliseconds that add up across hundreds of imports.

Circular Imports: The Edge Case That Bites

Consider this scenario:

# module_a.py
import module_b

# module_b.py
import module_a

When Python starts importing module_a, it: 1. Creates an empty placeholder for module_a in sys.modules 2. Begins executing module_a.py 3. Hits import module_b – starts the same process for module_b 4. Inside module_b, hits import module_a 5. Finds the partially initialized module_a in sys.modules

This is where it gets weird. The reference to module_a exists, but its module_a.x attributes might not exist yet because execution of module_a.py never finished. You get an AttributeError at runtime, not at import time.

The fix? Delay the import: move the circular import inside a function or method, so it only triggers when the module is fully loaded.

The __init__.py and Package Initialization

When you import a package (a directory with __init__.py), Python executes that __init__.py file before anything else. This file can: - Pre-load submodules - Define __all__ for explicit exports - Run setup code

But here's a subtle trap: if your __init__.py imports a submodule, and that submodule imports from the package, you've just created a circular import within your own codebase. Many package authors now leave __init__.py empty or mostly empty for this reason.

sys.meta_path: The Extensible Backbone

The import system isn't hardcoded—it's driven by a list of "finders" stored in sys.meta_path. By default, this list has: - BuiltinImporter (for built-in modules) - FrozenImporter (for frozen modules) - PathFinder (for everything else based on sys.path)

You can add your own finder to this list. This is how: - zipimport makes it possible to import from ZIP archives - Pytest's conftest.py discovery works - Custom import hooks for downloading packages at runtime (yes, people do this—use with caution)

The Performance Reality

Every import statement is an I/O operation (or at least a hash lookup). In large applications, imports can become the dominant startup cost. Here are practical numbers: - A simple import takes about 0.1-0.5ms if the module is already compiled - But a first-time import of a large library like NumPy can take 100-200ms just for the search and load phase

This is why production tools often use lazy imports—import numpy as np placed inside a function that gets called later, not at module load time. Python 3.7+ even has importlib.import_module() for runtime, dynamic imports.

What Actually Happens in Memory

After the module is loaded, Python stores everything in a module object's __dict__. This is a regular dictionary holding all the names defined in that module's global scope. When you write import foo, Python creates a reference in your current namespace's dictionary pointing to that module object.

That's all it is—a dictionary lookup. When you call foo.bar(), Python looks up foo in your local globals(), finds the module object, then looks up bar in that module's __dict__.

The Takeaway

Python's import system is elegant but not magical. Understanding it means you can: - Avoid circular import bugs by restructuring code or using lazy imports - Speed up startup by understanding where time goes - Write better packages with clean __init__.py files - Even build custom import hooks for specialized use cases

Next time you type import something, remember: you're not just including code—you're triggering a carefully orchestrated search, load, and cache system that's been refined over decades. And now you know exactly what's happening behind that one line.

Comments

Questions, corrections, and tips stay visible for everyone reading this page.

0 in thread

Join the discussion

Shown next to your comment.

Up to 4,000 characters

No comments yet

Be the first to leave a note — it helps the next reader.