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 Modules, Packages & Environments interview questions
More ways to practice
The self-quiz is live. Join our channel for updates, new content & tech tips.