Deep Engineering
Intermediate·Published·3.11 · 3.12 · 3.13 · 3.14·35 MIN

Modules and imports: a module runs once, and from copies a reference

Questions about imports seem like housekeeping until you are asked what exactly runs on import. A run answers: the module body ran once for a thousand and three imports — and again once its entry was deleted from sys.modules; after from settings import DEBUG, the assignment settings.DEBUG = True did not show the new value; a circular import failed on from and worked once the access to the module moved inside a function; and a file of your own named random.py next to the script shadowed the standard module.

Full technical treatment

TL;DR

A module is usually a .py file, and import does two things: it finds the module, running its body on the first successful load, and it binds a name. A loaded module is kept in the sys.modules dictionary of the current interpreter, and a repeated successful import under the same full name takes the finished object from there. In the run three import noisy in a row and another thousand in a loop ran the module body exactly once; after the entry was deleted from sys.modules, the next import ran it again.

Hence the main consequence: from settings import DEBUG is the assignment DEBUG = settings.DEBUG at the moment of import, not a live link to the module. After settings.DEBUG = True the imported name DEBUG stayed False. A list imported the same way is shared: an append through the module is visible, because there is one object.

Beyond that comes what separates understanding from having read about it. A circular import does not always fail: from app.a import X from a module that has not finished importing gives ImportError, while import app.a with the access app.a.X inside a function works. A file of your own named random.py next to the script shadows the standard module — since 3.13 the error says so outright. And a repeated import inside a function cost 84.3 ns on top of an empty call in the measurement on CPython 3.13.7, while the first import json in a fresh interpreter took 9.1 ms.

Runs: CPython 3.11.15, 3.12.3, 3.13.7 and 3.14.7 on Linux x86_64; checked on 14.10.2026. Python 3.15, released by that date, added explicit lazy imports (PEP 810); the lesson describes the ordinary, eager import and was not run on 3.15.

Where to start
Before this lesson it is enough to understand
  • how to run a script with python file.py;
  • what a function and a module-level global variable are;
  • that a dictionary stores values by key — the lesson "Built-in collections";
  • for the example with patching in tests — what mock.patch does: in detail in the lesson "Testing"; here "it temporarily replaces a name for the duration of a with block" is enough.
You do not need to know in advance
  • sys.modules, importlib.reload, from … import;
  • __name__ == "__main__", python -m, circular imports;
  • packages and __init__.py, the cost of the first and a repeated import.

Base: a module is usually a file, and import is two operations

A typical module is a .py file. Its names — functions, classes, top-level variables — become attributes of the module object. Full files — settings.py and main.py in one folder, run with python3 main.py:

PYTHON
# settings.py
DEBUG = False
 
# main.py
import settings
print(settings.DEBUG)   # False

A .py file is not the only kind of module: sys is built into the interpreter and has no file, and a C extension module is a .so file (on Windows, .pyd). A package is a module that contains other modules; a regular package is a folder with an __init__.py file, but a folder without one is imported as a package too (see "Deeper"). The rest of the lesson is about the ordinary case — .py files and folders with __init__.py.

A junior-level answer to "what does import do": it finds the module, runs it on the first load and makes its names available through the module name; from m import x takes one name out directly. That is correct, and it is followed by follow-up questions: does the module run on every import, how does from differ beyond syntax, and where do circular import errors come from. The documentation describes import as two steps:

The basic import statement (no "from" clause) is executed in two steps:

  1. find a module, loading and initializing it if necessary
  2. define a name or names in the local namespace for the scope where the "import" statement occurs.
— The language reference — the import statement

The key words here are "if necessary". The mechanisms below are about when that necessity arises and what follows from it.

How to read the code blocks: a block marked as a full file can be copied as is; a block without a language is the output of a run, in which this English version translates the captions, while program strings, identifiers, paths and error texts stay as in the run; the Russian original of the output is in bench/imports/runs/.

Mechanism 1: a module runs once — sys.modules

language contractThe cache of loaded modules and re-running through reload are the sys and importlib docstrings; the output is a run of bench/imports/basics.py, sections 1 and 7, identical on CPython 3.11–3.14 (Linux x86_64).

The noisy module prints a line when its body runs — and also checks whether it is already in sys.modules. The script imports it three times, then another thousand times in a loop, then reloads it:

1) a module runs once
   [the body of noisy runs; noisy already in sys.modules: True ]
   after three imports:
   noisy in sys.modules -> True
   again is noisy -> True
   sys.modules['noisy'] is noisy -> True
   another 1000 imports in a loop — the body did not run
   [the body of noisy runs; noisy already in sys.modules: True ]
   after reload — the body ran again
   reload returned the same module object -> True
   the name taken with from before reload is noisy.ITEMS -> False

The first import created the module object, put it into sys.modules — the dictionary of loaded modules, keyed by module name — and only then ran the body: the first line shows that the module is already there while the body runs. The behaviour of circular imports rests on this order (Mechanism 4). Every later import finds the module in the dictionary and only binds the name: import noisy as again gave the very same object.

importlib.reload runs the body again — in the same module object. But names taken with from before the reload point to the old objects: the ITEMS list imported earlier is no longer noisy.ITEMS.

reload is not the only way to run a module again. The cache is an ordinary dictionary of one interpreter, and the rule has limits. Section 7 of the same run (added on 14.10.2026), a fragment; output, captions translated:

   del sys.modules['noisy'] and import noisy again:
   [the body of noisy runs]
   [the body of noisy runs]
   a new module object -> True
   the first load failed — import tries again:
   [the body of flaky runs]
   RuntimeError: the first load failed
   'flaky' in sys.modules -> False
   [the body of flaky runs]
   'flaky' in sys.modules -> True
   another interpreter — its own sys.modules:
   [the body of noisy runs]
   [the body of noisy runs]

After del sys.modules['noisy'] the next import ran the body again and gave a new module object. The module flaky, which failed on its first load, did not stay in the cache, and the second import ran it once more — this time successfully. And a separate Python process (here launched through subprocess) ran the body anew: it has its own sys.modules. The precise rule: a repeated successful import under the same full name in the same interpreter takes the existing sys.modules entry.

Three practical consequences follow from this rule:

  • A module's top-level code runs on the first import. A database connection, reading a file, a print at module level will happen at the moment someone first imports the module — not earlier and not again.
  • A module is a natural singleton within an interpreter. An object created at module level is one per process: everyone who imports gets the same module. More precisely, one per name in sys.modules: the exception is covered in Mechanism 3, and deleting the entry from the cache creates a new one.
  • An import inside a function does not run the module again. After the first time it is a dictionary lookup — the cost is covered under "Deeper".

Mechanism 2: from m import x copies a reference

language contractWhat from puts into the namespace is the language reference, the import statement; the output is a run of bench/imports/basics.py, section 2, identical on CPython 3.11–3.14.

For the from form the documentation describes what happens to each name:

a reference to that value is stored in the local namespace

— The language reference — the import statement

A reference to the value — not to the name in the module. from settings import DEBUG works like DEBUG = settings.DEBUG executed once. The settings module declares DEBUG = False and ITEMS = []:

2) from m import x copies a reference rather than linking names
   settings.DEBUG -> True
   DEBUG          -> False
   settings.ITEMS -> ['x']
   ITEMS          -> ['x']
   ITEMS is settings.ITEMS -> True

The assignment settings.DEBUG = True bound the name DEBUG in the module to a new object, while the importer's name DEBUG still points to the old one — False. A list is different: append does not rebind a name but changes the object itself, and the module and the importer share it. It is the same rule as "assignment binds a name, while a change to an object is visible to everyone who refers to it", only across modules.

The practical consequence is patching in tests. The module report does from settings import DEBUG, and its function show() returns DEBUG:

   patching settings.DEBUG = True before the first import report:
   report.show() -> True
   patching settings.DEBUG = True after import report:
   report.show() -> False
   mock.patch after import report:
   mock.patch('settings.DEBUG') -> report.show() = False
   mock.patch('report.DEBUG')   -> report.show() = True

A patch before the first import of report works — from will copy the already patched value. After the import it does not reach report: it has its own name. That is why mock.patch is applied where the name is looked up — report.DEBUG — or report writes import settings and reads settings.DEBUG at the moment of use.

Mechanism 3: __name__ == "__main__"

language contractThe name of the main module and the __name__ attribute are the execution model and the data model; the output is a run of bench/imports/basics.py, sections 3 and 7, identical on CPython 3.11–3.14.

The name used to uniquely identify the module in the import system. For a directly executed module, this will be set to "main".

— The execution model and the data model — the module name

One and the same file tool.py — run as a script, imported, and run with -m:

3) __name__: one file — run and import
   python tool.py:
   __name__ = '__main__'
   the if __name__ == '__main__' block ran
   python -c 'import tool':
   __name__ = 'tool'
   python -m tool:
   __name__ = '__main__'
   the if __name__ == '__main__' block ran

Why the check is needed follows from Mechanism 1: on import the module body runs in full. Code that should run only when the file is launched — parsing command-line arguments, starting a server — is put under if __name__ == "__main__":. Without it, import tool from a test or another module would start the program.

This is also where the exception to "a module runs once" comes from. The launched file sits in sys.modules under the name __main__, and if something imports it by its file name, that is a second, separate module:

   python selfimp.py — the script imports itself:
   the body runs, __name__ = '__main__'
   the body runs, __name__ = 'selfimp'
   selfimp is sys.modules['__main__'] -> False

The body ran twice, and the two copies have different top-level objects. This happens when an application module is both the entry point and imported by other modules.

A module of a package is run with -m. A full example of two files in the folder shop/; the commands are run from the folder that contains shop/. The code is as in the run; the comments are translated.

PYTHON
# shop/__init__.py — full file
PRICE = 10
PYTHON
# shop/cli.py — full file
import sys
 
from . import PRICE
 
print("__name__ =", repr(__name__))
print("__package__ =", repr(__package__))
print("sys.path[0] =", sys.path[0])
print("PRICE =", PRICE)

The output of python3 -m shop.cli and python3 shop/cli.py is section 7 of the run; for the second command the last line of the traceback is shown (output, captions translated):

   python -m shop.cli:
   __name__ = '__main__'
   __package__ = 'shop'
   sys.path[0] = <folder>
   PRICE = 10
   python shop/cli.py:
   ImportError: attempted relative import with no known parent package

-m shop.cli looks the module up by its full name, like import: first the package shop, then its module cli. That is why __package__ is 'shop', and the relative import from . import PRICE knows what the dot counts from. The current folder comes first in the search path (sys.path[0] is the one containing shop/). The same file run by its path is just a script: it has no package, and the relative import fails. __name__ is '__main__' in both cases.

Mechanism 4: circular imports — where they fail and where they do not

language contractThe output is a run of bench/imports/basics.py, section 4; the modules of the cycle sit in the package app/. The error texts for the package are identical on CPython 3.11–3.14; for files directly in the launch folder the text differs on 3.13 and 3.14 — see below.

The module app.a imports app.b, and app.b imports app.a. While app.a is running it is already in sys.modules, but it has only run up to the import line: the name X is not in it yet. The outcome depends on when app.b accesses X:

4) circular import
   app/a: from app.b import Y; app/b: from app.a import X — import app.a:
   ImportError: cannot import name 'X' from partially initialized module 'app.a' (most likely due to a circular import) (<folder>/app/a.py)
   app/a: import app.b; app/b: import app.a and app.a.X at module level:
   AttributeError: cannot access submodule 'a' of module 'app' (most likely due to a circular import)
   app/a: import app.b; app/b: import app.a and app.a.X inside a function:
   app.b.y() -> 2
  • from app.a import X needs X at the moment of import — and the module is partially initialized, only partly executed. ImportError.
  • import app.a and app.a.X at module level — the error comes even before X. A submodule becomes an attribute of its package only once it has run in full, while app.a is already in sys.modules but only partly executed — hence cannot access submodule 'a' of module 'app'.
  • import app.a and app.a.X inside a function — works. By the time the function is called both modules have run to the end, and X exists.

Hence the ways to break a cycle: access the other module's name at the moment of use rather than of import; move the import inside a function; extract the shared part into a third module that both depend on. The last is usually the right one: a cycle most often means that two modules share something that should live on its own.

One version detail. If the same two files sit not in a package but directly in the launch folder, then on 3.13 and 3.14 the hint about the cycle in the error text was replaced by a hint about renaming:

   the same a.py and b.py (the from variant) directly in the launch folder — import a:
   ImportError: cannot import name 'X' from 'a' (consider renaming '<folder>/a.py' if it has the same name as a library you intended to import)

On 3.11 and 3.12 the same place had the text partially initialized module 'a' (most likely due to a circular import). The cause of the cycle is the same — only the hint changed, and the new hint makes the cycle easy to miss.

Mechanism 5: a file of your own named like a standard module

language contractThe order of the search path is the sys docstring; the output is a run of bench/imports/basics.py, section 5: the hint in the error text is there on CPython 3.13 and 3.14, and not on 3.11 and 3.12.

The folder of the script being run comes first in the module search path — the sys docstring says "path[0] is the script directory". A file random.py placed next to the script game.py is found before the standard module:

5) your own random.py next to the script
   [my random.py runs]
   AttributeError: module 'random' has no attribute 'randint' (consider renaming '<folder>/random.py' since it has the same name as the standard library module named 'random' and prevents importing that standard library module)

Your own file ran, not the standard module, and it has no random.randint. The renaming hint was added in 3.13; on 3.11 and 3.12 the error ends at has no attribute 'randint', and the cause has to be guessed. The classic cases are files random.py, email.py, csv.py or json.py, created to experiment with the module of the same name.

Deeper: packages and the cost of an import

measured observationThe package is bench/imports/basics.py, sections 6 and 7, identical on CPython 3.11–3.14 except for the line about _json: whether it is built in or a separate file depends on the build. The cost is bench/imports/cost.py: medians of nine rounds, CPython 3.13.7 on Linux x86_64, one machine (2 cores); 3.14.7 is in the record cost-3.14.txt. These are observations of one configuration: on another machine and version the numbers and their ratios will differ — measure on your own.

A regular package is a folder of modules with an __init__.py file; that file is the body of the package itself. Importing a submodule runs the package first, while importing the package does not load the submodules by itself:

6) a package: __init__.py and a submodule
   import pkg.sub:
   [pkg/__init__.py]
   [pkg/sub.py]
   pkg.sub.NAME -> sub
   import pkg, then pkg.sub:
   [pkg/__init__.py]
   hasattr(pkg, "sub") -> False
   AttributeError: module 'pkg' has no attribute 'sub'
   import json — json/__init__.py loaded the submodules:
   'json.decoder' in sys.modules -> True
   'json.encoder' in sys.modules -> True

If a package wants pkg.sub to be available after import pkg, its __init__.py imports the submodule itself. That is how json is built: after a single import json its submodules json.decoder and json.encoder are already loaded. So importing a package can pull in more modules than the code shows.

A package without __init__.py is a namespace package: it has no body, and its parts can lie in several folders of the search path. Section 7 of the run — a folder nspkg/ with one file mod.py, and a built-in module too (output, captions translated):

   a package without __init__.py (namespace package):
   [nspkg/mod.py]
   nspkg.__file__ -> None
   list(nspkg.__path__) -> ['<folder>/nspkg']
   a built-in module and an extension module:
   sys.__spec__.origin -> 'built-in'
   hasattr(sys, '__file__') -> False
   _json -> built into the interpreter

import nspkg.mod worked without __init__.py; such a package has no file, only the list of folders __path__. sys does not even have __file__: it is built in. And the helper _json is built in in the 3.13.7 and 3.14.7 builds, while in the 3.11.15 and 3.12.3 builds it is a separate .so extension module: that is how particular builds are made, not the language.

The cost. The first import finds the file, loads the code and runs the body; a repeated one is a lookup in sys.modules:

1) a repeated import: import json inside a function, ns per call
   form                             min    median       max
   empty function                  40.3      41.4      83.1
   json from globals               43.0      44.3      91.8
   import json inside             122.0     125.7     185.8
   import json on top of the empty function by medians: 84.3 ns

2) the first import in a fresh interpreter, µs per import statement
   module                           min    median       max
   import json                   8220.2    9128.1    9720.3
   import decimal                3160.0    3595.0    7370.9
   import asyncio               51559.8   54409.2   69681.2
   first import json / repeated by medians: 108256

A call of a function with import json inside costs 125.7 ns, with json from the global names 44.3 ns, an empty one 41.4: the repeated import itself adds 84.3 ns. That is more than a global name, but it is tens of nanoseconds. The first import of json takes 9.1 ms, a hundred thousand times more, and asyncio 54 ms. That is why an import inside a function is used not for the speed of the call but for startup: a heavy module needed by one rare command should not be loaded on every launch of the program.

How to answer in an interview

Short answer: import finds a module and binds a name. The module body runs on the first successful load; a repeated import under the same full name in the same interpreter takes the module from the sys.modules dictionary. from m import x copies a reference to the value at the moment of import — a new assignment to m.x does not change it. if __name__ == "__main__" separates launch code from code that will run on import.

That is enough for a correct answer. What follows is what you add if the interviewer digs.

If the interviewer digs deeper

On "how many times does a module run". "Once per interpreter and full name. In my run three imports in a row and another thousand in a loop ran the body once: the module sits in sys.modules. It runs again with importlib.reload, with a new import after del sys.modules[...], with a retry after a failed first load, and in another process. And a script imported by its name: it already exists as __main__, and the body runs a second time."

On "why isn't the new value visible after from". "from settings import DEBUG is like DEBUG = settings.DEBUG once. An assignment in the module rebinds its name, while mine stays on the old object. A mutable object is shared, and its changes are visible."

On "how do you deal with a circular import". "It fails when the name is needed at the moment of import: from a import X from a module that is still running. It works if you access a.X inside a function. The right fix is to extract the shared part into a third module."

On "is an import inside a function slow?" "No, after the first time it is a dictionary lookup: 84 ns on my machine. The first import is the slow one, milliseconds. That is why heavy modules are imported inside a function, to speed up startup."

Next they ask

Next they ask

How do you reload a module, and what will not be updated?

Short answer

importlib.reload(module) runs the body again in the same module object — in the run the body's line printed a second time, and reload returned the same object. But names taken with from still point to the old objects: reload rebinds the names inside the module, not in those who copied them — in the run the old ITEMS was no longer noisy.ITEMS.

Next they ask

How does python -m tool differ from python tool.py?

Short answer

In both cases the module gets __name__ == "__main__" — in the run the block ran both times. The difference is in how the module is found: -m looks it up by name, like import, which is why modules inside packages (python -m pkg.tool) and standard library modules — python -m json.tool, python -m venv — are run this way. A package module run with -m has __package__ filled in, and relative imports work; run by its file path, it gives ImportError: attempted relative import with no known parent package (a full example is in Mechanism 3).

Next they ask

Where do you put an import — at the top of the file or in a function?

Short answer

By convention, at the top of the file: the module's dependencies are visible at once. An import is moved inside a function for two reasons: to break a circular import and to avoid loading a heavy module until it is needed. A repeated import inside a function costs tens of nanoseconds, so it does not get in the way of the call.

Common misconceptions

Claim

An import inside a function runs the module again on every call

Actually

The module body runs only on the first import. In the run another thousand import statements in a loop never ran it, and a repeated import json cost 84.3 ns.

Claim

A module is always a .py file, and a package is always a folder with __init__.py

Actually

These are the typical cases. sys is built in and has no file; an extension module is a .so or .pyd file; a folder without __init__.py is imported as a namespace package: in the run import nspkg.mod worked, and nspkg.__file__ was None.

Claim

from settings import DEBUG gives a live link to the module's variable

Actually

It is a copy of the reference to the value at the moment of import. After settings.DEBUG = True the imported DEBUG stayed False.

Claim

A circular import always leads to an error

Actually

What fails is an access to a name that does not exist yet. import app.a with the access app.a.X inside a function works: by the time of the call both modules have run.

Claim

import pkg loads all the submodules of the package

Actually

Only pkg/__init__.py runs. In the run, after import pkg it was hasattr(pkg, "sub") -> False.

Claim

The name of your own file does not matter

Actually

A file random.py next to the script shadows the standard module: import random finds it first.

Version history

VersionChangeWhat it means for code
3.11The reference point of the lesson. The import behaviour in all six scenes is the same on 3.11–3.14; only the error texts change.
3.13Two error texts changed in the run. For a shadowed standard module the error suggests renaming the file: consider renaming '…/random.py' since it has the same name as the standard library module …. For cycle files directly in the launch folder, the hint about the cycle was replaced by a hint about renaming. Tests that compare these texts in full will break on 3.13.
3.14In the run the error texts are the same as on 3.13. The language reference describes the second step of an import as binding a name in the current namespace — just as an assignment statement would.
3.15Released on 9 October 2026; the lesson was not run on it. According to the release notes (PEP 810), "explicit lazy imports using the new lazy soft keyword … defers the actual module loading until the imported name is first used". Everything the lesson says about the module body running at the moment of import refers to the ordinary, eager import — without lazy.

Practice

Two problems. Answer first, then check against the real output: in both, the correct answer is taken from a run of the script rather than assigned.

Practice · predict the output

config.py has one line: DEBUG = False. What will this code print?
import config
from config import DEBUG
config.DEBUG = True
print(DEBUG, config.DEBUG)

Practice · estimate

The body of the module counter increments a counter by one. The module is imported 1000 times in a loop, then importlib.reload is called twice. How many times did the body run?
times

Check yourself

Question 1 of 4

The module db.py opens a database connection at its top level. Five modules of the program import it. How many connections will be opened?

How it was measured

The numbers and observations of this lesson come from these scripts. Each one opens right from here — together with the record of its run: what it ran on, what came out and with what spread.

Runs: CPython 3.11.15, 3.12.3, 3.13.7 and 3.14.7, Linux x86_64, one machine (2 cores); documentation quotes were checked on 14.10.2026. Python 3.15 was out by that date, but it is not in the measurement environment.

Sources & further reading

4 SOURCES

  1. The language reference — the import statementOfficial documentation. What import consists of: «The basic import statement (no "from" clause) is executed in two steps: 1. find a module, loading and initializing it if necessary 2. define a name or names in the local namespace for the scope where the "import" statement occurs». And what from does for each name: «a reference to that value is stored in the local namespace». The 3.14 text says current instead of local and adds to the second step: «just as an assignment statement would». The text was checked against the built-in pydoc help (the pydoc_data.topics module) of CPython 3.13.7 and 3.14.7.https://docs.python.org/3.13/reference/simple_stmts.html#the-import-statement
  2. The execution model and the data model — the module nameOfficial documentation. When a module is created and what the main one is called: «The namespace for a module is automatically created the first time a module is imported. The main module for a script is always called "__main__"». On the __name__ attribute: «The name used to uniquely identify the module in the import system. For a directly executed module, this will be set to "__main__"». Checked against pydoc_data.topics of 3.13.7 and 3.14.7.https://docs.python.org/3.13/reference/executionmodel.html#naming-and-binding
  3. sys.modules and importlib.reloadOfficial documentation. Docstrings in the interpreter. The sys module: «modules -- dictionary of loaded modules». The importlib.reload function: «Reload the module and return it. The module must have been successfully imported before». And where modules are looked up: «path -- module search path; path[0] is the script directory, else ''». Checked by calling sys.__doc__ and importlib.reload.__doc__ on 3.13.7 and 3.14.7.https://docs.python.org/3.13/library/importlib.html#importlib.reload
  4. What's new in Python 3.15 — explicit lazy imports (PEP 810)Official documentation. Python 3.15 was released on 9 October 2026. The release notes: «explicit lazy imports using the new lazy soft keyword … defers the actual module loading until the imported name is first used». The quote was checked on 14.10.2026. The lesson was not run on 3.15: the measurement environment does not have that version; everything the lesson says about running a module body refers to an ordinary import.https://docs.python.org/3.15/whatsnew/3.15.html