How Python Imports Work: sys.modules and Circular Imports

A Python module is a file run once on first import and cached in sys.modules. What from-import copies, the main guard and how to fix circular imports.

  • Course: Python study plan
  • Module: Modules, packages and imports
  • Kind: Lesson
  • Reading time: 14 min
  • Runtime: CPython 3.11

What happens when you import a module in Python?

Python first looks the module up in sys.modules; if it is cached, import simply binds the name. Otherwise it finds the file on the import path, sys.path, which starts with the script's own directory, creates a module object, caches it, runs the file top to bottom in that module's namespace and binds the name. Top-level code therefore runs once per process.

Lesson

A module is a file; importing it runs the file once and gives you an object whose attributes are the file's top-level names. That sentence covers most of what people find mysterious about imports — why a module's top-level code runs on first import and never again, why two imports of the same module give the same object, why from x import y copies a binding rather than linking to a variable, and why circular imports fail the way they do. This lesson covers the import forms, what import does step by step, the module cache, __name__, the import path, and the conventions for ordering and style.

The forms

import math                          # binds the name math to the module object
import numpy as np                   # an alias
from math import sqrt, pi            # binds sqrt and pi directly
from math import sqrt as root        # aliased
from os.path import join             # a name from a submodule
from math import *                   # every public name — avoid outside the REPL

import math gives one name and you write math.sqrt; from math import sqrt gives the function itself. The first keeps the origin visible at every use and cannot collide; the second is shorter. Convention: import module for modules you use many names from or whose names are generic (os, json, re), from module import name for a few specific names, and the alias forms only for the community-standard ones (np, pd) or to dodge a clash.

What import does

import x performs, in order:

  1. If x is already in sys.modules (the cache), bind the name to that object and stop.
  2. Otherwise find the file: the current script's directory, then each entry of sys.path (PYTHONPATH, the standard library, site-packages).
  3. Create an empty module object and put it in sys.modules before running the code (this is what makes circular imports partially work).
  4. Execute the file top to bottom in the module's own namespace: every def, class and assignment binds a name on the module.
  5. Bind x in the importing namespace.

So top-level code runs once per process, at the first import, wherever that happens. A module that prints or opens a file at top level does so at import time — one reason work belongs in functions and under if __name__ == "__main__":. Importing the same module again anywhere is a dictionary lookup.

from-import copies a binding

# config.py
DEBUG = False
def enable():
    global DEBUG
    DEBUG = True

# main.py
from config import DEBUG, enable
enable()
print(DEBUG)          # False — main's DEBUG still points at the old object

import config
config.enable()
print(config.DEBUG)   # True — reading through the module sees the rebinding

from config import DEBUG binds main.DEBUG to whatever object config.DEBUG was at import time. Rebinding config.DEBUG later does not touch main.DEBUG. Mutable objects are shared (a list imported by name reflects appends), rebinding is not. Access module-level state that changes through the module.

__name__ and the main guard

Each module has __name__: its import name, or "__main__" for the script being run. The guard if __name__ == "__main__": (Module 1) is what lets one file be imported for its functions and run for its behaviour. python -m package.module runs a module as __main__ by import name, which is what makes relative imports (next lesson) work in a script.

The module object

import math
type(math)                   # <class 'module'>
math.__name__                # 'math'
math.__file__                # the path (built-in modules like sys have none)
dir(math)                    # every attribute
math.__dict__                # the namespace as a dict
vars(math) is math.__dict__  # True

A module is a plain object: attributes can be read with getattr, added by assignment (a bad habit outside tests), and the namespace inspected. importlib.import_module("pkg.mod") imports by a string name — for plugins and dynamic loading; importlib.reload(mod) re-executes a module in place, which is a REPL convenience and not something a program should rely on.

Circular imports

a.py imports b, and b.py imports a. Importing a puts a half-empty a in sys.modules, starts running a, hits import b, runs b — which finds a in the cache and gets the half-initialised object. If b only uses a's names inside functions called later, it works; if b does from a import something at top level before a has defined it, ImportError: cannot import name. The fixes, in order of preference: move the shared thing into a third module both import; import inside the function that needs it; import the module (import a) and access a.something lazily. A cycle is usually a sign that two modules are one module or that a dependency points the wrong way.

Style

  • Imports at the top of the file, one per line, in three groups separated by blank lines: standard library, third-party, your own — each alphabetical. isort and ruff do this automatically (Module 15).
  • No import * in modules: it hides where names come from and can shadow silently.
  • No import inside a function unless breaking a cycle or deferring an expensive optional dependency.
  • Names of modules are lowercase, short, without hyphens (a hyphen cannot be imported).
  • Never name your file after a standard module (random.py, test.py, json.py): the script's directory comes first on the path, and import random will find yours.

Pitfalls

  • Top-level side effects that run at import.
  • Expecting from m import x to track later rebindings of m.x.
  • A file named like a standard module shadowing it.
  • Circular top-level from imports.
  • import * hiding a name collision.
  • Relying on reload in a running program.

Key takeaways

  • A module is a file; import finds it, runs it once, caches it in sys.modules and binds a name; later imports are lookups.
  • import m keeps names qualified; from m import x copies the binding at import time — rebinding through the module does not reach it.
  • __name__ is the import name or "__main__"; the guard separates library use from script use.
  • Circular imports fail when a from import needs a name not yet defined; restructure, or import lazily.
  • Imports at the top in three sorted groups; never import *; never shadow a standard module's name.

Common questions

What is the difference between import module and from module import name?

import math binds one name to the module, and you write math.sqrt, which keeps the origin visible and cannot collide. from math import sqrt binds the function itself. It copies the binding at import time, so a later rebinding of the module's variable is not seen through the imported name.

How do I fix "ImportError: cannot import name" caused by a circular import?

The error means two modules import each other and one does a top-level from import of a name the other has not defined yet. Move the shared code into a third module both import, import inside the function that needs it, or use import a and read a.name later.

What does if __name__ == "__main__" mean in Python?

Every module has __name__: its import name, or "__main__" for the script being run. The if __name__ == "__main__": guard runs code only when the file is executed directly, so the same file can be imported for its functions without triggering its script behaviour.

Why does a file named random.py break import random?

The script's own directory comes first on the import path, so import random finds your random.py instead of the standard-library module, and the functions you expected are missing. Rename the file, and never name one after a standard module such as json.py or test.py.

Should Python imports go at the top of the file?

Yes. PEP 8 puts imports at the top, one per line, in three groups separated by blank lines (standard library, third-party, then your own), each alphabetical. Import inside a function only to break a cycle or defer an expensive optional dependency, and avoid import * in modules.

Exercises

Import order, simulated

Simulate the import system. Lines module <name> imports <a> <b> … declare what each module imports at its top level (a module may import nothing). Lines run <name> import that module: a module not yet in the cache has its body started — print its name — and its imports are processed in order (each recursively) before the body finishes; a module already in the cache is not run again. For each run, print the names of the modules whose bodies started, in order, or (cached) if none.

Input: lines. Output: one line per run.

module a imports b c
module b imports c
module c imports
run a
run c
run b

prints

a b c
(cached)
(cached)

A binding is a copy

Build a real module at run time: create types.ModuleType("config"), execute the source DEBUG = False plus an enable()/disable() pair that rebind the module-level DEBUG with global, register it in sys.modules, then do from config import DEBUG, enable, disable and import config. For each command line (enable or disable) call the function and print the imported name DEBUG next to config.DEBUG, showing that the from-import copied the binding.

Input: command lines. Output: imported=<bool> module=<bool> per command.

enable
disable

prints

imported=False module=True
imported=False module=False

In this module: Modules, packages and imports

← Checkpoint — Iterators, generators and itertools · Packages — directories, __init__.py, relative imports and project layout →