Skip to content

The Import System Interview Questions & Answers

15 questions Updated 2026-06-18 Share:

Python interview questions on the import system — modules vs packages, absolute vs relative imports, __main__, sys.path module resolution, import caching, and circular imports.

Read the in-depth guidePython Import System Explained — Modules, sys.path, Caching, and Absolute vs Relative Imports(opens in new tab)
15 of 15

A module is a single .py file — a namespace of functions, classes, and variables you can import. A package is a directory of modules; it groups related modules under one dotted namespace (mypkg.utils).

Historically a package needed an __init__.py file to be recognized; that file runs when the package is first imported and can expose a curated public API. Since Python 3.3, a directory without __init__.py can still be a namespace package, but a regular package with __init__.py is the common, explicit choice.

# file layout
# mypkg/
#   __init__.py      <- makes it a regular package
#   utils.py         <- a module inside the package

import mypkg.utils          # import a module from a package
from mypkg import utils     # same module, bound as `utils`

Why it matters: modules are the unit of code reuse, packages are the unit of organization — both are objects at runtime (module.__file__, package.__path__).

An absolute import spells out the full path from a top-level package (from mypkg.utils import helper). A relative import uses leading dots to navigate relative to the current module's package — one dot for the current package, two for the parent.

# inside mypkg/sub/thing.py
from mypkg.utils import helper   # absolute — explicit, unambiguous
from ..utils import helper       # relative — '..' = mypkg, then .utils
from . import sibling            # relative — same package

Relative imports only work inside a package and only when the module is run as part of that package — running the file directly with python thing.py breaks them (ImportError: attempted relative import with no known parent package).

Rule of thumb: PEP 8 prefers absolute imports for clarity; reach for relative imports inside large packages to avoid repeating a long package prefix.

Every module has a __name__ variable. When a file is run directly, Python sets its __name__ to the string "__main__". When the same file is imported, __name__ is set to the module's name instead. The guard therefore runs code only when the file is executed as a script, not when it's imported.

def main():
    print("running as a program")

if __name__ == "__main__":   # True only via `python myfile.py`
    main()                   # skipped when `import myfile`

This lets a file double as both an importable library and a runnable program: tests and other modules can import its functions without triggering the script logic.

Rule of thumb: put executable entry-point code under the guard so importing the module stays free of side effects.

On import x, Python searches a list of finders and, for file-based modules, walks sys.path — a list of directories — in order, using the first match. sys.path is built from the script's directory (or cwd), the PYTHONPATH environment variable, and the installation's standard library / site-packages.

import sys
print(sys.path)        # ['', '/usr/lib/python3.12', '.../site-packages', ...]
# '' (or the script dir) is searched first — local files can SHADOW stdlib!

# a file named random.py in your folder will hide the real `random` module

Built-in modules and frozen modules are found before sys.path is consulted, which is why you can't shadow sys itself.

Why it matters: a local file named like a stdlib module (queue.py, email.py) silently shadows the real one — a classic, confusing import bug.

sys.modules is a cache mapping module names to already-imported module objects. The first import of a module executes its code top to bottom and stores the result there; every later import of the same name just returns the cached object — so module code runs once per interpreter session.

A circular import is when module A imports B while B imports A. Because the importing module is added to sys.modules before its body finishes running, the second import gets a partially-initialized module — names defined later in the file aren't there yet.

# a.py
import b                 # starts importing b...
def helper(): ...

# b.py
import a                 # a is in sys.modules but only HALF-defined
print(a.helper)          # AttributeError — helper isn't bound yet

Fixes: move the import inside the function that needs it (deferred until call time), restructure to remove the cycle, or import the module object rather than names from it. Rule of thumb: circular imports usually signal that two modules should share a third.

import * binds all of a module's public names (those not starting with _) into the current namespace. If the module defines __all__ (a list of names), only those are imported — giving the author control over the public API.

# mymod.py
__all__ = ["public_fn"]      # restricts what `import *` exposes
def public_fn(): ...
def _private(): ...
helper = 1

# elsewhere:
from mymod import *          # only public_fn is imported

Rule of thumb: avoid import * in real code (it pollutes the namespace and hides origins); define __all__ to document a package's public surface.

import x binds the module object (x.y to access members). from x import y binds y directly. The latter is a one-time snapshot — if x later rebinds y, your imported y won't see the change.

import math
math.pi              # access via the module

from math import pi
pi                   # bound directly

# snapshot pitfall:
from mod import counter   # captures the value now
mod.counter = 99          # your `counter` is unchanged

Rule of thumb: use import x to keep names traceable and see later changes; use from x import y for frequently-used names, accepting the snapshot semantics.

Importing inside a function defers the cost until the function is called — useful for heavy/optional dependencies, faster startup, and breaking circular imports. The module is still cached after the first call.

def export_pdf(data):
    import reportlab      # only loaded if this feature is used
    ...

def needs_sibling():
    from . import other   # avoids a top-level circular import
    return other.func()

Rule of thumb: keep imports at module top by default; move them into functions only for optional heavy deps, startup speed, or to break import cycles.

importlib.reload(mod) re-executes a module's code and updates the existing module object in place. But objects already imported elsewhere or instances of old classes keep referencing the old code, leading to confusing inconsistencies.

import importlib, mymod
importlib.reload(mymod)     # re-runs mymod's top-level code

# gotcha: existing `from mymod import f` bindings still point to the OLD f,
# and old instances aren't migrated to the reloaded class

Rule of thumb: reload is for interactive/REPL tinkering only; in real apps restart the process rather than rely on reload's partial, error-prone updates.

__init__.py runs when the package is first imported. It marks a regular package and is the place to expose a curated API (re-export submodule names), set package __all__, or run package-level setup. It can be empty.

# mypkg/__init__.py
from .core import main_function     # expose at package level
from .utils import helper
__all__ = ["main_function", "helper"]

# now users can do:
from mypkg import main_function     # instead of mypkg.core.main_function

Rule of thumb: keep __init__.py light; use it to flatten/curate the public API, not for heavy work that slows every import of the package.

Wrap the import in try/except ImportError and fall back to an alternative or a flag. This is the standard pattern for optional features and library compatibility shims.

try:
    import ujson as json      # fast optional lib
except ImportError:
    import json               # stdlib fallback

try:
    import numpy
    HAS_NUMPY = True
except ImportError:
    HAS_NUMPY = False

Rule of thumb: use try/except ImportError to degrade gracefully when an optional package is missing — provide a fallback or a capability flag.

python -m mypkg runs the package's __main__.py as a script while keeping proper package context, so relative imports work. -m mod runs a module by its import name rather than file path — the recommended way to run package entry points.

python -m http.server        # run a stdlib module as a tool
python -m mypkg              # runs mypkg/__main__.py with package context
python myfile.py             # runs as top-level script — relative imports break

Rule of thumb: launch package code with python -m pkg (not python pkg/file.py) so __package__ is set and relative imports resolve correctly.

A namespace package (PEP 420) is a package without __init__.py whose contents can be spread across multiple directories on sys.path. Python merges them into one logical package — handy for splitting a large namespace across separate distributions.

# site-packages/acme/tools/...      (from package acme-tools)
# site-packages/acme/data/...       (from package acme-data)
# both contribute to the single `acme` namespace, no __init__.py needed
import acme.tools
import acme.data      # both resolve under one `acme` namespace

Rule of thumb: use namespace packages to let independently-shipped subpackages share a common top-level name; use a regular __init__.py package otherwise.

Modules expose dunder attributes: __name__ (import name or "__main__"), __file__ (source path), __doc__ (module docstring), __dict__ (its namespace), and for packages __path__ (search locations).

import os
os.__name__        # 'os'
os.__file__        # '/usr/lib/python3.12/os.py'
os.__doc__         # the module docstring
import json
json.__path__      # package search paths (packages only)

Rule of thumb: __name__, __file__, and __doc__ are the everyday module attributes — useful for introspection, logging, and locating resources.

Python compiles each module to bytecode and caches it as a .pyc file in __pycache__ to skip recompilation on later imports. It checks the source's timestamp/hash and recompiles only when the .py changes. It's purely a speed optimization.

mymod.py
__pycache__/
    mymod.cpython-312.pyc   # cached bytecode, tagged by interpreter version

Rule of thumb: __pycache__/.pyc are auto-managed caches — safe to delete, safe to gitignore; they speed up imports, not execution of the code itself.

More ways to practice

The self-quiz is live. Join our channel for updates, new content & tech tips.

Join our WhatsApp Channel