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.
- 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.patchdoes: in detail in the lesson "Testing"; here "it temporarily replaces a name for the duration of awithblock" is enough.
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:
# settings.py
DEBUG = False
# main.py
import settings
print(settings.DEBUG) # FalseA .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:
- find a module, loading and initializing it if necessary
- define a name or names in the local namespace for the scope where the
"import" statement occurs.
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
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
printat 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
importinside 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
For the from form the documentation describes what happens to each name:
a reference to that value is stored in the local namespace
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__"
The name used to uniquely identify the module in the import system. For a
directly executed module, this will be set to "main".
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.
# shop/__init__.py — full file
PRICE = 10# 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
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 XneedsXat the moment of import — and the module ispartially initialized, only partly executed.ImportError.import app.aandapp.a.Xat module level — the error comes even beforeX. A submodule becomes an attribute of its package only once it has run in full, whileapp.ais already insys.modulesbut only partly executed — hencecannot access submodule 'a' of module 'app'.import app.aandapp.a.Xinside a function — works. By the time the function is called both modules have run to the end, andXexists.
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
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
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
How do you reload a module, and what will not be updated?
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.
How does python -m tool differ from python tool.py?
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).
Where do you put an import — at the top of the file or in a function?
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
An import inside a function runs the module again on every call
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.
A module is always a .py file, and a package is always a folder with __init__.py
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.
from settings import DEBUG gives a live link to the module's variable
It is a copy of the reference to the value at the moment of import. After settings.DEBUG = True the imported DEBUG stayed False.
A circular import always leads to an error
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.
import pkg loads all the submodules of the package
Only pkg/__init__.py runs. In the run, after import pkg it was hasattr(pkg, "sub") -> False.
The name of your own file does not matter
A file random.py next to the script shadows the standard module: import random finds it first.
Version history
| Version | Change | What it means for code |
|---|---|---|
| 3.11 | The 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.13 | Two 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.14 | In 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.15 | Released 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
import config from config import DEBUG config.DEBUG = True print(DEBUG, config.DEBUG)
Practice · estimate
Check yourself
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.
This is neither a retelling nor a separate text: everything below is taken from the article itself — its own summary, the section headings, the “actually” column and the version table. Which is why these theses cannot drift from the article.
The gist
- A module is usually a
.pyfile, andimportdoes 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 thesys.modulesdictionary of the current interpreter, and a repeated successfulimportunder the same full name takes the finished object from there. In the run threeimport noisyin a row and another thousand in a loop ran the module body exactly once; after the entry was deleted fromsys.modules, the nextimportran it again. - Hence the main consequence:
from settings import DEBUGis the assignmentDEBUG = settings.DEBUGat the moment of import, not a live link to the module. Aftersettings.DEBUG = Truethe imported nameDEBUGstayedFalse. A list imported the same way is shared: anappendthrough 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 Xfrom a module that has not finished importing givesImportError, whileimport app.awith the accessapp.a.Xinside a function works. A file of your own namedrandom.pynext to the script shadows the standard module — since 3.13 the error says so outright. And a repeatedimportinside a function cost 84.3 ns on top of an empty call in the measurement on CPython 3.13.7, while the firstimport jsonin 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
importand was not run on 3.15.
In fact
- The module body runs only on the first import. In the run another thousand
importstatements in a loop never ran it, and a repeatedimport jsoncost 84.3 ns. - These are the typical cases.
sysis built in and has no file; an extension module is a.soor.pydfile; a folder without__init__.pyis imported as a namespace package: in the runimport nspkg.modworked, andnspkg.__file__wasNone. - It is a copy of the reference to the value at the moment of import. After
settings.DEBUG = Truethe importedDEBUGstayedFalse. - What fails is an access to a name that does not exist yet.
import app.awith the accessapp.a.Xinside a function works: by the time of the call both modules have run. - Only
pkg/__init__.pyruns. In the run, afterimport pkgit washasattr(pkg, "sub") -> False. - A file
random.pynext to the script shadows the standard module:import randomfinds it first.
By version
- 3.11
- The 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.13
- Two 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.14
- In 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.15
- Released 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
importrefers to the ordinary, eager import — withoutlazy.<
What is covered
- Base: a module is usually a file, and import is two operations
- Mechanism 1: a module runs once — sys.modules
- Mechanism 2: from m import x copies a reference
- Mechanism 3: `__name__ == "__main__"`
- Mechanism 4: circular imports — where they fail and where they do not
- Mechanism 5: a file of your own named like a standard module
- Deeper: packages and the cost of an import
- How to answer in an interview
- Next they ask
- Common misconceptions
- Version history
- Practice
- Check yourself
- How it was measured
Sources & further reading
4 SOURCES
- 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
- 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
- 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
- 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
lazyimports using the newlazysoft 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