Problem
Two lifecycle state dictionaries are keyed by id(click_context) and stored in Click's shared
Context.meta mapping:
_LIFECYCLE_CAPTURE_META_KEY — captures.setdefault(id(click_context), {})
(lib/python/base_cli/_lifecycle_install.py:95-96), read back at line 543-544;
_LIFECYCLE_RESOLUTION_META_KEY — resolution_map[id(click_context)] = resolution
(lib/python/base_cli/_lifecycle_install.py:579), read back as
resolution_map.get(id(parent)) at line 542.
Context.meta is inherited from the parent, so a single dict is shared by the whole context tree
for the entire invocation. Neither dict holds a reference to the context it keys, and neither is
ever pruned. Click closes and releases child contexts as an invocation proceeds — most visibly in
chain=True groups and in nested group dispatch — so a freed context's id() can be reused by a
context created later in the same invocation. The later context would then read the earlier one's
captured lifecycle values, or be treated as the parent of an unrelated resolution.
id() reuse after deallocation is normal CPython behaviour, not a pathological case. The
consequence would be a silently wrong --debug/--quiet/--config/--environment value for one
command in a chain, which is hard to notice and harder to reproduce. The dicts also grow for the
life of the invocation, one entry per context.
Verified evidence
Reviewed 2026-09-30 at a58ec109349fa3f3d03eae5b0de078b39ea361a2. This is a static finding from
reading the code; I did not construct an input that forces the collision, so it is reported as a
latent correctness risk rather than a reproduced failure. What is directly verifiable:
$ grep -n "_LIFECYCLE_CAPTURE_META_KEY\|_LIFECYCLE_RESOLUTION_META_KEY\|id(click_context)" lib/python/base_cli/*.py
_lifecycle_install.py:95: captures = click_context.meta.setdefault(_LIFECYCLE_CAPTURE_META_KEY, {})
_lifecycle_install.py:96: context_values = captures.setdefault(id(click_context), {})
_lifecycle_install.py:543: captures = click_context.meta.get(_LIFECYCLE_CAPTURE_META_KEY, {})
_lifecycle_install.py:544: context_captures = captures.get(id(click_context), {})
_lifecycle_install.py:579: resolution_map[id(click_context)] = resolution
No deletion site exists for either key, and no code path keeps the keyed contexts alive.
Proposal
Replace identity-by-id() with a key whose lifetime is bound to the context:
- simplest: use the context object itself as the dict key. Click contexts are hashable by identity,
and holding the reference both prevents id reuse and makes the lifetime explicit; the dicts die
with meta at the end of the invocation.
- or: stamp a unique token on each context (
click_context.meta-adjacent attribute or a
monotonically increasing counter set on first capture) and key by that.
Either way, add a _selected_click_paths-style cleanup or accept bounded growth explicitly, and
add a comment recording why the key choice matters.
Acceptance criteria
- No lifecycle state is keyed by
id() of an object that can be deallocated during the invocation.
- A regression test exercises a
chain=True group with per-command lifecycle values and asserts each
command observes its own resolved values, with the intermediate contexts released between members.
- Memory behaviour for a long chain is documented or bounded.
Non-goals
- Do not change the public
get_lifecycle_values() contract or the LIFECYCLE_META_KEY guard.
Problem
Two lifecycle state dictionaries are keyed by
id(click_context)and stored in Click's sharedContext.metamapping:_LIFECYCLE_CAPTURE_META_KEY—captures.setdefault(id(click_context), {})(
lib/python/base_cli/_lifecycle_install.py:95-96), read back at line 543-544;_LIFECYCLE_RESOLUTION_META_KEY—resolution_map[id(click_context)] = resolution(
lib/python/base_cli/_lifecycle_install.py:579), read back asresolution_map.get(id(parent))at line 542.Context.metais inherited from the parent, so a single dict is shared by the whole context treefor the entire invocation. Neither dict holds a reference to the context it keys, and neither is
ever pruned. Click closes and releases child contexts as an invocation proceeds — most visibly in
chain=Truegroups and in nested group dispatch — so a freed context'sid()can be reused by acontext created later in the same invocation. The later context would then read the earlier one's
captured lifecycle values, or be treated as the parent of an unrelated resolution.
id()reuse after deallocation is normal CPython behaviour, not a pathological case. Theconsequence would be a silently wrong
--debug/--quiet/--config/--environmentvalue for onecommand in a chain, which is hard to notice and harder to reproduce. The dicts also grow for the
life of the invocation, one entry per context.
Verified evidence
Reviewed 2026-09-30 at
a58ec109349fa3f3d03eae5b0de078b39ea361a2. This is a static finding fromreading the code; I did not construct an input that forces the collision, so it is reported as a
latent correctness risk rather than a reproduced failure. What is directly verifiable:
No deletion site exists for either key, and no code path keeps the keyed contexts alive.
Proposal
Replace identity-by-
id()with a key whose lifetime is bound to the context:and holding the reference both prevents
idreuse and makes the lifetime explicit; the dicts diewith
metaat the end of the invocation.click_context.meta-adjacent attribute or amonotonically increasing counter set on first capture) and key by that.
Either way, add a
_selected_click_paths-style cleanup or accept bounded growth explicitly, andadd a comment recording why the key choice matters.
Acceptance criteria
id()of an object that can be deallocated during the invocation.chain=Truegroup with per-command lifecycle values and asserts eachcommand observes its own resolved values, with the intermediate contexts released between members.
Non-goals
get_lifecycle_values()contract or theLIFECYCLE_META_KEYguard.