Lesson 16 / 16
Reloadability and the Main Block
The same file creates one module object when imported and a second when run as a script, and the main block runs only in the second; reload keeps the module object but renews every function inside it.
Contents
In the previous lesson, everything was imported: the library modules and the measurement functions alike were defined inside a body and called from outside. Yet the same file can work in two separate forms. One is the form measured up to here — the file is requested as a module, its body runs, and its names are opened up for others to use. The other is running it directly: the file is launched as a script and becomes the program’s entry point.
The same body runs in both forms. The only thing that differs is what gets written into the module’s own name, and a block that looks at that name runs in only one case. This lesson’s question is: when the same file is run in two forms, how many module objects get created, which record holds which, and what happens to old objects taken from a module when it gets reloaded?
The Module’s Own Name
Every module object’s dictionary has an entry named __name__, and import fills
that entry with the module’s name in the cache. When a file is imported, the
file’s module name gets written there. When the same file is run directly, "__main__"
gets written there instead, because that run has no import name — it was not
requested, it was launched.
The main block is the condition that reads this distinction. A block at the end
of the body that compares __name__ to "__main__" runs when the file is run as a
script, and does not run when it is imported. This is how a file can be both a
library and an entry point: definitions sit in the body and are open to everyone,
running sits inside the block and only works on a direct run.
This has a measurable consequence, and it does not contradict the rule established
in the first lesson, it follows from it: a cache record is kept by name. A file
run as a script gets registered under the name __main__, the same file imported
under its own name. Two separate names, two separate records, meaning two separate
module objects — from the same file.
What Reload Changes
When the module cache drops a record, the next request creates a new object; this was measured in the first lesson. Reload, by contrast, is a different job: it reruns the body, but writes into the existing module object’s dictionary. The module object stays the same, the names inside it get refreshed.
The distinction has two observable consequences, and the measurement tests both: because the body reruns, every definition produces a new object, and the write is done without emptying the dictionary.
The measurement’s assumptions:
- MP29 — The measurement leaves no file in the repository; sources are produced in a temporary directory and the directory’s path is not printed.
- MP30 — The measured file writes its own name to a shared list on every run; the counting side is the measured file itself.
- MP31 — The script run opens no separate process. It is run in the same
interpreter, with the module’s own name set to
__main__; this is why the measurement can read the shared list. - MP32 — The main block only drops a record into the list; no other work is done inside it.
- MP33 — Before reloading, a single string in the source text is changed; the only thing that changes is the value the function returns.
- MP34 — Before reloading, a name is added to the module from outside, and that name’s fate is separately read.
- MP35 — The old function object is bound to a separate name before reloading;
comparisons are made with
isand no identity number is printed. - MP36 — When the measurement ends, the added path entry and all cache records are rolled back.
Measurement
"""Main block and reload: how many module objects does the same file create.""" import importlib import runpy import sys import tempfile from pathlib import Path TOOL = """import log log.EVENTS.append(('body', __name__)) def greet(): return 'first' if __name__ == '__main__': log.EVENTS.append(('main block', __name__)) """ LOG_SOURCE = "EVENTS = []\n" def setup(root): (root / "log.py").write_text(LOG_SOURCE, encoding="utf-8") (root / "tool.py").write_text(TOOL, encoding="utf-8") sys.path.insert(0, str(root)) importlib.invalidate_caches() def teardown(root): sys.path.remove(str(root)) for name in ("log", "tool"): sys.modules.pop(name, None) def measure(root): setup(root) log = importlib.import_module("log") tool = importlib.import_module("tool") import_run = list(log.EVENTS) log.EVENTS.clear() script = runpy.run_module("tool", run_name="__main__") script_events = list(log.EVENTS) separate_object = script["greet"] is not tool.greet cache_intact = sys.modules["tool"] is tool log.EVENTS.clear() tool.ADDED = "before reload" old_greet = tool.greet (root / "tool.py").write_text(TOOL.replace("'first'", "'second'"), encoding="utf-8") importlib.invalidate_caches() reloaded = importlib.reload(tool) result = { "import_run": import_run, "script": script_events, "separate_object": separate_object, "cache_intact": cache_intact, "reload_events": list(log.EVENTS), "same_module": reloaded is tool, "old_function": old_greet is tool.greet, "old_value": old_greet(), "new_value": tool.greet(), "added_survived": getattr(tool, "ADDED", "deleted"), } teardown(root) return result with tempfile.TemporaryDirectory() as d: s = measure(Path(d)) print(f"ran during import: {s['import_run']}") print(f"ran when run as a script: {s['script']}") print(f"function in the script namespace is a separate object from the module's -> {s['separate_object']}") print(f"the cache record is not disturbed -> {s['cache_intact']}") print() print(f"ran during reload: {s['reload_events']}") print(f"module object is the same -> {s['same_module']}, " f"function object is the same -> {s['old_function']}") print(f"the function bound to the old name gives {s['old_value']}, " f"the module's function gives {s['new_value']}") print(f"name added before reload -> {s['added_survived']}")
ran during import: [('body', 'tool')]
ran when run as a script: [('body', '__main__'), ('main block', '__main__')]
function in the script namespace is a separate object from the module's -> True
the cache record is not disturbed -> True
ran during reload: [('body', 'tool')]
module object is the same -> True, function object is the same -> False
the function bound to the old name gives first, the module's function gives second
name added before reload -> before reload
Two Runs, Two Objects
The first two lines give the main block’s rule directly. The only thing that ran on
import is the body, and the module’s own name is tool; there is no main block
record. Running as a script reruns the same body, but this time the module’s own
name is __main__, and the main block runs too. Same file, same lines, two
different results — the only thing making the difference is the value written to
the name.
The third line shows the cost: the function in the script run’s namespace is
a separate object from the module’s function. Because the body ran a second
time, the def statement created a second function object. Module objects created
from the same file: 2, shared between them: 0. The fourth line completes
this: the tool record in the cache is not disturbed, it is still bound to the
first object. The two runs stand side by side and do not see each other.
This explains why writing work into the main block is limited. If a file gets both
imported and run as a script, the two runs produce two separate namespaces
carrying the same names. A counter or cache kept at module level gets created
twice; what gets written into one is not visible in the other. The only reason this
does not show up in the measurement is that the counted list sits not in tool but
in the log module both share. The rule is: an entry point is a shell — state
and definitions stay in the body, the main block only calls a function.
Reload
The lower section measures a separate mechanism. What ran during reload is again
the body, and the module’s own name stays as tool — this is the difference
from the script run. The next line gives the heart of the distinction: the module
object is the same, the function object is not. Because the body reran,
def created a new function object, and the name in the module’s dictionary got
bound to it; the module object itself never changed.
The third line shows whose view this breaks. The side that took the function under its own name before reloading holds the old object, and that object gives first; the function called through the module gives second. Only those reading the change from the module’s dictionary saw it. Function objects created: 2, held: 2, shared: 0. The distinction the first lesson measured is paid off again here: import shares the object, it copies the name — a copied name does not get refreshed.
The last line confirms the dictionary was not emptied: the name added to the module from outside before reloading is still there. Reload is not a cleanup, it is an overwrite. Every name with no counterpart in the new body — a removed definition, an attribute added from outside — keeps its old value.
What the two measurements say together is: rerunning a source has three separate outcomes. If the cache record is removed and it is requested again, a new module object gets created; if it is reloaded, the module object is kept, its contents get renewed; if it is run as a script, a second object gets created under a second name, and the first stays as it was. In all three, the body runs one more time, but the objects created and held are different in all three.
Summary
- The module’s own name is filled with the module name on import, with
__main__on a direct run; the main block only runs in the second case. - When the same file is run in two forms, 2 module objects get created; the definitions the two namespaces share is 0, and the cache record from the import is not disturbed.
- The main block is written as a shell: definitions and state stay in the body, because the body runs in both forms and every module-level piece of state gets created twice.
- Reload keeps the module object, reruns the body, and binds the names inside it to new objects; an old function object taken beforehand keeps giving its old value.
- Reload does not empty the dictionary; names with no counterpart in the new body stay in place with their old values.
Course Wrap-Up
The course ran on a single question: how many objects did the written form create, how many did it hold, what did it share? The result itself was never counted as the measure in any lesson, because the counts of two notations giving the same result diverged every time.
| Lesson | Form measured | Created / held / shared |
|---|---|---|
collections/01 Lists |
a thousand-item list and eight growth forms | 1000 created and held; binding a second name creates 0, [one] * 5 creates 0 and shares one item across five slots |
collections/02 Tuples |
six operations immutability closes off and t += (new,) |
1 created; the container is new, items are shared (big[0] is tup[0] → True); multiple return creates 0 |
collections/03 Dictionaries |
membership on two hundred items and the keys() view |
200 comparisons in a list, 1 in a set and dict; the view shares the dict and grows from 2 to 3, a snapshot copy holds 1000 slots |
collections/04 Sets |
deduplicating three hundred items and six set operations | 15050 comparisons by hand, 200 with a set; six operations create 0 items and every object in the result is shared |
collections/05 Specialized Collections |
three notations of the same grouping and trimming a thousand-item stream | setdefault creates 320, a default dict 100 containers; in the stream all three notations produce 1000, held is 1000 against 5; returning creates 3 intermediate containers with a list, 0 with a deque |
collections/06 Slicing |
eight slice forms and copying a nested list | all eight create 0 items and return a new container; in a nested list 4 get produced and a slice copy shares both inner lists, a deep copy gives 4 |
iteration/01 Iterator Protocol |
the iter and next contract, three traversal forms |
iter writes 0 references, list() writes 3, a comprehension creates 3 new objects; the first two share items, the last shares 0 |
iteration/02 Generators |
calling a function containing yield, and yield from |
at setup a comprehension creates 1000, a generator 0; at three items it stays at 3, yield from stays at 1 up to the first item |
iteration/03 Generator Expressions |
two notations of summing a thousand items | both create 1000 and give 499500; held is 1000 against 0 in the list |
iteration/04 Comprehension Syntax |
list, set, dict comprehension, and where the filter condition sits | 1000 / 100 / 100 held, shared 1000 / 0 / 100; filtering before production creates 5, after creates 10 |
iteration/05 Lambda and Higher-Order Functions |
how many times the key function is called, and map with filter |
key called once per item, 200 calls total; a hand-written sort makes 39800; sorted builds a new container and shares 200 items, map creates 0 at setup |
iteration/06 Decorators |
the layers wrapping builds and preserving metadata | unwrapped 1, one decorator 2, two decorators 3 layers; the object bound to the name changes, the original function is shared (single.__wrapped__ is compute → True); a memoizing wrapper reduces 20 requests to 5 calls and holds 5 items |
modules-and-packages/01 The Module System |
three import requests and search path order | 2 module objects created, 1 held; two names share the same object and the shared log module stays at 1 |
modules-and-packages/02 Package Structure |
the object relative and absolute notation resolve to | a single request runs 5 bodies and holds 5 objects; relative and absolute share 0 (is → False) |
modules-and-packages/03 A Tour of the Standard Library |
the same three jobs written with the library and by hand | by hand creates 800 and holds 800, with the library creates 3 and holds 0; the result is 3 in both |
modules-and-packages/04 Reloadability and the Main Block |
import, script run, and reload | a script run creates a 2nd module object, the first is kept, shared definitions 0; reload shares the module, renews the function |
Four readings cut across the table. Laziness does not reduce production, it reduces holding: in summing a thousand items, both notations produce 1000 objects and both give 499500; the only place they part is what is held in the list. A copy is one layer deep: every operation that builds a new container renews the outer container and shares what is inside it — in tuples, in slicing, in set operations alike. What decides the cost is not the data but the form of access: the same two hundred items ask for 200 comparisons in a list, 1 in a set. A module is an object too, and it gets created once: the last topic carried the same counting scheme past the file boundary and measured that import is an act of creation too.
Throughout the course, the containers stood ready-made. List, tuple, dict, set, generator, and module — all were objects the language gave, and what was measured was the cost of choosing them. The next course stands on the other side of this boundary. Choosing a container and writing a container are different things: an object having a length, being comparable, and being able to serve as a key in a container is not a chosen trait, it is a contract that gets written. The Object-Oriented Python and Types course builds the side that writes that contract: classes that fit the language’s syntax, situations where composition is chosen over inheritance, and contracts made explicit with type hints. Here, how many objects each container cost was counted; there, the object that will stand in its place will itself be written.
To keep your progress and take notes, Log in
My notes
Log in to take notes.