pydantic/monty · error
bubble-up captured '{name_str}' that is bound nowhere — scop
Error message
bubble-up captured '{name_str}' that is bound nowhere — scope analysis bug What it means
When a child scope's capture is bubbled up to its parent during scope finalization, the captured name must exist in the parent's cell vars or enclosing locals. If it is bound in neither, preparation panics with a message explicitly flagging a scope-analysis bug — the capture should never have been predicted.
Source
Thrown at crates/monty/src/prepare.rs:442
}
let PrepareState::Function(state) = &mut self.state else {
return Ok(());
};
if state.cell_var_map.contains_key(&captured_name) || state.free_var_map.contains_key(&captured_name) {
return Ok(());
}
if state.assigned_names.contains(&captured_name) || state.locals.contains(captured_name) {
let slot = state.locals.ensure_slot(captured_name, position)?;
state.cell_var_map.insert(captured_name, slot);
} else if state.enclosing_locals.contains(&captured_name) {
let slot = state.locals.ensure_slot(captured_name, position)?;
state.free_var_map.insert(captured_name, slot);
} else {
let name_str = self.interner.get_str(captured_name);
panic!("bubble-up captured '{name_str}' that is bound nowhere — scope analysis bug");
}
Ok(())
}
/// Builds the parallel free-var slot vectors for a just-prepared child
/// scope from its `free_var_map` (`name -> the child's own slot`).
///
/// Returns `(free_var_slots, free_var_enclosing_slots)`: the first holds the
/// child's own slots (where it installs each captured cell at call time);
/// the second holds OUR slot it reads that cell from when the closure is
/// built (via [`Self::lookup_captured_slot`]). Both are ordered by the
/// child slot so they stay index-aligned.
fn build_free_var_slots(
&mut self,
inner_free_var_map: AHashMap<StringId, NamespaceId>,
) -> (Vec<NamespaceId>, Vec<CaptureSource>) {
let mut entries: Vec<_> = inner_free_var_map.into_iter().collect();
entries.sort_by_key(|(_, inner_slot)| *inner_slot);View on GitHub (pinned to adc986b362)
Solutions
- Fix capture prediction so only names actually bound in the chain are marked captured.
- Check that cell_var_map and enclosing_locals are merged from ancestor scopes before finalization.
- Reproduce with a minimal Python snippet and compare against CPython's closure behavior to find the missing binding site.
Example fix
// before
panic!("bubble-up captured '{name_str}' that is bound nowhere — scope analysis bug");
// after
// skip the spurious capture and let name resolution fall back to globals
return Ok(()); // once prediction is fixed to not emit it Defensive patterns
Strategy: validation
Validate before calling
// guard before bubbling up a capture assert!(state.cell_var_map.contains_key(&captured_name) || state.enclosing_locals.contains(&captured_name));
Prevention
- Merge ancestor cell/local maps before finalizing child scopes
- Write nested-closure test cases spanning function, class and comprehension scopes
- Only mark captures for names verified bound in the chain
When it happens
Trigger: Finalizing a nested scope whose captured name the parent never bound: e.g. the analysis recorded a capture from a pass that ran before the parent's binding information was complete, or a name is bound only in a scope not on the capture chain.
Common situations: Contributors editing finalize_child_scope or the capture-prediction logic; test cases with deeply nested closures and class scopes where capture chains are hard to predict.
Understand the failure class
Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.
Related errors
- free_var '{name_str}' not found in enclosing scope's cells,
- class member '{member_name}' missing from class-body locals
- cycle values are not hashable
- {} python values are not hashable
- gather commit frame id is not a GatherFuture
AI-assisted analysis of pydantic/monty@adc986b362 (2026-09-13).
Data as JSON: /api/errors/ffb0e628c8b52640.
Report an issue: GitHub.