Skip to content
  • Modules, Packages & Imports · Python

Modules, Packages & Imports

LearnViaNotes @learnvianotes· 5 min read

A module is a .py file; a package is a folder of modules. import finds, runs and caches them — understanding that cycle explains __name__, circular imports and sys.modules.

Importing

Python
import math                          # whole module
from collections import Counter      # one name
import datetime as dt                # alias
print(math.sqrt(16), Counter("aab")["a"], dt.date(2026, 1, 1).year)   # 4.0 2 2026
  1. Check the cache

    If the name is already in sys.modules, that module object is returned immediately.

  2. Find

    Otherwise Python searches sys.path: the script's folder, PYTHONPATH, the standard library, then site-packages.

  3. Execute once

    A new module object is created and the file's top-level code runs in its namespace.

  4. Cache & bind

    The module is stored in sys.modules and the name is bound in the importing namespace.

FormBindsUse when
import jsonjsonDefault — the call site shows where names come from (json.dumps)
import numpy as npnpConventional short aliases
from pathlib import PathPathNames you use a lot
from os import *Every public nameAlmost never — pollutes the namespace and hides origins

Imports run once and are cached

Python
import importlib, sys, tempfile
from pathlib import Path

folder = tempfile.mkdtemp()
Path(folder, "lvn_greet.py").write_text('print("module body runs")\nVALUE = 1\n')
sys.path.insert(0, folder)

import lvn_greet                  # runs the file: prints "module body runs"
import lvn_greet                  # cached in sys.modules: nothing printed
print("lvn_greet" in sys.modules, lvn_greet.VALUE)

lvn_greet.VALUE = 99
importlib.reload(lvn_greet)       # re-executes the file (prints again), resetting VALUE
print(lvn_greet.VALUE)

sys.path.remove(folder)
del sys.modules["lvn_greet"]
Output
module body runs
True 1
module body runs
1

if __name__ == "__main__":

Python
# utils.py
def add(a, b):
    return a + b

if __name__ == "__main__":    # True only when run directly: python utils.py
    print(add(2, 3))          # 5

Module attributes

Python
import json, os

print(json.__name__, os.path.__name__)                   # json posixpath
print(json.__file__.endswith("json/__init__.py"))        # True   ← json is a package
print(json.__doc__.splitlines()[0])                      # JSON (JavaScript Object Notation) <https://json.org> is a subset of
print(__name__)                                          # __main__
AttributeHolds
__name__The module's import name ("__main__" for the script being run)
__file__Path of the source file (absent for built-in modules like sys)
__doc__The module docstring — the first string literal in the file
__all__Names exported by from module import *
__path__Only on packages: where to look for submodules

__all__ and import *

Python
import sys, tempfile
from pathlib import Path

folder = tempfile.mkdtemp()
Path(folder, "lvn_shapes.py").write_text(
    "__all__ = ['area']\n"
    "def area(r): return 3 * r * r\n"
    "def debug(): pass\n"
    "def _helper(): pass\n"
)
sys.path.insert(0, folder)
from lvn_shapes import *
print("area" in dir(), "debug" in dir(), "_helper" in dir())   # True False False
sys.path.remove(folder)

Without __all__, import * brings in every name that doesn't start with _. With it, only the listed names — __all__ is how a module declares its public API.

Packages

Layout
shop/
├── __init__.py        # marks a regular package; runs on "import shop"
├── __main__.py        # runs on "python -m shop"
├── cart.py
└── payments/
    ├── __init__.py
    └── upi.py

from shop.cart import Cart            # absolute import (preferred)
from .cart import Cart                # relative import (only inside the package)
from ..payments import upi            # two dots = parent package

Importing by name: importlib

Python
import importlib

mod = importlib.import_module("json")                     # module name as a string
print(mod.dumps([1, 2]))                                  # [1, 2]

plugin = "collections"                                    # e.g. read from a config file
Counter = getattr(importlib.import_module(plugin), "Counter")
print(Counter("aab")["a"])                                # 2

Circular imports

a.py imports b, and b.py imports a. The second import finds a half-initialised in sys.modules, so names defined later in a don't exist yet.

Typical error
ImportError: cannot import name 'User' from partially initialized module 'models'
(most likely due to a circular import)
  1. Restructure: move the shared code into a third module both can import (usually the real fix).

  2. Import the module, not the name: import models and use models.User at call time.

  3. Move the import inside the function that needs it (deferred import).

  4. For type hints only, guard with if typing.TYPE_CHECKING: and use string annotations.

Virtual environments

Each project should install its dependencies into its own virtual environment — see Virtual Environments, pip & Packaging for the full workflow.

Terminal
$ python3 -m venv .venv && source .venv/bin/activate
(.venv) $ pip install requests

What happens when you import a module twice?

How do you fix a circular import?

Free for students and teachers

Write notes like this one

Cards for every idea, quizzes and flashcards, read aloud — on the web and your phone.

Something wrong with this note? Open it to report