← Back to Blogs

Deep Dive: Python Imports and the Role of __init__.py

By CoilPad • December 28, 2025 · 10 min read

We all use import every day. It's the first thing you type in almost every Python script. But for many developers, what happens after you type import pandas and hit enter remains a bit of a mystery.

In this guide, we'll peel back the layers of Python's import system. We'll explore how Python locates your code, how it decides what to load, and how you can leverage __init__.py to build cleaner, more professional packages.

The Import Mechanism

When you run import my_module, Python doesn't just blindly look at files. It follows a rigorous, step-by-step process.

1. The Search Path (sys.path)

Python looks up the list of directories defined in sys.path. This list typically includes:

  • The directory of the input script (or the current working directory).
  • PYTHONPATH (if set).
  • Standard library locations.
  • Site-packages (where pip installs libraries).

2. sys.modules: The Cache

Before searching sys.path, Python checks sys.modules. This is a dictionary that maps module names to module objects.

  • Hit: If the module is already in sys.modules, Python returns it immediately. It does not reload the file.
  • Miss: If it's not found, Python proceeds to search.
Note: This caching mechanism is why re-importing a module in the same session (like in a Jupyter notebook or REPL) often doesn't update your code changes. You'd need importlib.reload() to force a refresh.

3. Finders and Loaders

Python uses "Finders" to scour sys.path to see if they can locate the module. If a finder succeeds, it returns a "Spec" (specification) which contains a "Loader". The Loader is then responsible for actually executing the module's code and creating the module object.

The Role of __init__.py

You likely know that adding an __init__.py file to a directory turns it into a Python package. But it's much more than just a flag. It's a regular Python file that executes the moment your package is imported.

1. Package Initialization

Any code in __init__.py runs immediately. You use this to set up package-level data.

# mypackage/__init__.py
print("Initializing mypackage...")

# Database connection or configuration can go here
default_timeout = 30

2. Namespace Flattening

This is a professional pattern. Imagine you have a deep internal structure:

mypackage/
    utils/
        string_tools.py  (contains function `clean_text`)
        math_tools.py    (contains function `calculate_sum`)
    models/
        user.py          (contains class `User`)

Without help, users have to type:
from mypackage.utils.string_tools import clean_text

You can use __init__.py to import these into the top level:

# mypackage/__init__.py
from .utils.string_tools import clean_text
from .models.user import User

__all__ = ['clean_text', 'User']

Now users can simply do:
from mypackage import clean_text, User

3. Controlling Exports with __all__

In the example above, we defined __all__. This list controls what is imported when a user types from mypackage import *.

  • If __all__ is set, only those names are imported.
  • If __all__ is missing, Python imports everything that doesn't start with an underscore (_).
  • Best Practice: Always define __all__ in your public APIs to avoid polluting the user's namespace with internal implementation details.

Parsing Imports: Regex vs. AST

If you are building tools that need to analyze Python code (like a linter or a dependency checker), you might be tempted to use Regular Expressions (Regex) to find imports.

Don't do it.

Why Regex Fails

Python imports can be complex and multiline:

from my_long_module_name import (
    function_one,
    function_two,
    function_three as f3
)

Regex struggles to reliably parse nested parentheses, multiline statements, and comments.

The Solution: AST (Abstract Syntax Tree)

Python has a built-in ast module that parses code exactly like the interpreter does. It is 100% accurate.

import ast

code = """
import os
from collections import namedtuple
"""

tree = ast.parse(code)

for node in ast.walk(tree):
    if isinstance(node, ast.Import):
        for alias in node.names:
            print(f"Imported: {alias.name}")
    elif isinstance(node, ast.ImportFrom):
        print(f"From {node.module} imported...")

This approach is robust, handles all formatting quirks, and is the professional standard for static analysis.

Try It in CoilPad

Experiment with Python imports and packages locally.
See how your code works with instant feedback.

Download CoilPad