On this page
Scoped cleanup lesser conduits๏
๐ก Intermediate ยท Lesson 29
Scopes that END. A lesser conduit is a scope you can THROW AWAY: build one for a job, meld what the job needs, then cleanup() the child - its per-conduit creations are disposed through the book's disposal vocabulary, and the root keeps working untouched. THE LIFECYCLE LAW: with a DI runtime YOU own the lifecycles. The runtime holds references to what it built, so the GC cannot free what the world still holds - cleanup() is how memory comes back. (Beginner lesson 41 proves this with a weakref; here it becomes a working pattern.)
Before you run๏
Use the Intermediate guide for prerequisite concepts. Run from a checkout with Melder installed and Python 3.14 free-threading selected. The collection download includes the level's local helper modules.
Run the saved script๏
python UX_and_AIX_experiences/02_intermediate/29_scoped_cleanup_lesser_conduits.py
py -3.14t UX_and_AIX_experiences/02_intermediate/29_scoped_cleanup_lesser_conduits.py
Public surface๏
create_lesser_conduit as a disposable scope, conduit.cleanup() on the child only, disposal_method_names firing scope-locally
Code๏
1"""
2TIER: intermediate (29)
3GOAL: Scopes that END. A lesser conduit is a scope you can THROW AWAY:
4 build one for a job, meld what the job needs, then cleanup() the
5 child - its per-conduit creations are disposed through the book's
6 disposal vocabulary, and the root keeps working untouched.
7 THE LIFECYCLE LAW: with a DI runtime YOU own the lifecycles. The
8 runtime holds references to what it built, so the GC cannot free
9 what the world still holds - cleanup() is how memory comes back.
10 (Beginner lesson 41 proves this with a weakref; here it becomes a
11 working pattern.)
12SURFACE EXERCISED: create_lesser_conduit as a disposable scope,
13 conduit.cleanup() on the child only,
14 disposal_method_names firing scope-locally
15"""
16import sys
17from pathlib import Path
18
19sys.path.insert(0, str(Path(__file__).parent)) # local helper (see _dynamic_world)
20from _dynamic_world import dynamic_spellbook
21
22import melder as md
23
24
25class JobSession:
26 def __init__(self) -> None:
27 self.closed = False
28
29 def close(self) -> None:
30 self.closed = True
31
32
33def main() -> None:
34 book = md.Spellbook()
35 book.bind(spell=JobSession, existence="unique_per_conduit",
36 disposal_method_names=["close"])
37 root = book.conjure()
38
39 # The long-lived scope holds its own session...
40 root_session = root.meld(spell=JobSession)
41
42 # ...and each job gets a THROWAWAY scope with its own.
43 job = root.create_lesser_conduit()
44 job_session = job.meld(spell=JobSession)
45 assert job_session is not root_session # per-conduit = per-scope
46
47 # Job done: end the SCOPE, not the world.
48 job.cleanup()
49 print("job scope closed its session:", job_session.closed)
50 print("root session untouched:", root_session.closed is False)
51
52 # The root never noticed - it keeps resolving.
53 assert root.meld(spell=JobSession) is root_session
54 print("root still resolves after the child scope ended")
55
56 # And when the whole world is done, the same verb one level up.
57 root.cleanup()
58 print("root cleanup closed its session too:", root_session.closed)
59
60
61if __name__ == "__main__":
62 main()
Check the outcome๏
The script contains its own assertions or demonstrated refusal paths. Run it to evaluate those checks against your installed version. The code above is taken directly from the saved file; no run output is invented here.