A Binding is a scope you can carry around¶
Level: 301 · someone who has used instance_eval and wants to know what binding hands to ERB
One line: binding packages a method's local variables, its self and its name into an object that eval, ERB#result and local_variable_get can read and write later — Python's locals() is only a dict snapshot, eval/exec take explicit namespaces, and a frame object is the nearest thing to a Binding.
Kernel#binding returns a Binding for the scope it is called in, and the binding keeps that scope alive after the method has returned — it is a closure with no code attached. local_variables lists the names, local_variable_get and local_variable_set read and write them, receiver is the self of the scope, and eval(string, b) (or b.eval(string)) runs code as if it had been written there. Writing through the binding changes the method's own variable, not a copy: after local_variable_set(:a, 10) a later eval("a * b", b) sees 10 (row 4).
A name the method never had is different. The parser decides a method's local variables at compile time, so local_variable_set(:c, 3) with a new name cannot add a local to the method — it adds c to the binding object (rows 5 and 6), and eval("w = 7", b) does the same (row 7). That is why binding.local_variable_set(:c, 3) inside a method leaves defined?(c) nil and a fresh binding unaware of c: each binding call makes a new object, and the variable lived in the one that was thrown away. Keep the binding and it remembers everything, including changes made by later eval calls (row 8, a counter). A binding also knows __method__ and self (row 9); TOPLEVEL_BINDING is the file's own scope, whose receiver is main (row 10); and every Proc carries the binding it was created in (row 11). ERB is the everyday use: ERB.new(template).result(b) evaluates each <%= %> against the binding's variables (row 12), and a name the binding does not have raises NameError (row 13).
Verified output of binding_and_eval_rb.rb — regenerated by tools/run_examples.py, never hand-typed.
1. b = two_locals; b.class, b.local_variables [Binding, [:a, :b]]
2. b.local_variable_get(:a) 1
3. eval("a + b", b) 3
4. after b.local_variable_set(:a, 10): eval("a * b", b) 20
5. local_variable_set(:c, 3) adds c to the binding [true, [:a, :b, :c]]
6. in a method, set through a throwaway binding [nil, false]
7. eval("w = 7", b); b.local_variable_get(:w) 7
8. 3.times { cb.eval("count += 1") }; count 3
9. eval("__method__", b), b.receiver [:two_locals, main]
10. TOPLEVEL_BINDING.receiver, .local_variable_get(:z) [main, 5]
11. a lambda's binding: .local_variables, z [[:z], 5]
12. ERB.new("a is <%= a %>, b is <%= b %>").result(b) "a is 10, b is 2"
13. eval("nope", b) raises NameError
Reading the output¶
Rows 1–3: two_locals returns its binding; the binding lists a and b, reads a, and evaluates a + b. Row 4: a write through the binding is a write to the variable. Rows 5–7: three ways to create a variable that exists only in the binding — local_variable_set on a kept binding (c appears in local_variables), local_variable_set on a throwaway binding inside the method (defined?(c) is nil and the next binding says false), and an assignment inside eval. Row 8: the binding returned by counter_binding outlives the call and count is incremented three times by three separate evals. Row 9: the binding knows it came from two_locals and that self there was main. Row 10: TOPLEVEL_BINDING sees the file's locals (z). Row 11: the lambda from make_multiplier carries a binding whose only local is z. Row 12: ERB reads a (now 10) and b from the binding. Row 13: an unknown name is a NameError, the same error a bare nope would raise.
Compared with Python¶
Python has no Binding type. locals() returns a plain dict (row 1), which reads like a binding (rows 2–3) but is not connected to the function once it has been returned: writing into it changes the dict, not any variable (row 4 works only because the dict is the namespace we pass to eval). Row 5 is trivially True — adding a key to a dict — and row 6 is the real counterpart of Ruby's throwaway binding: locals()["c"] = 3 inside a function does not create c, and reading c is a NameError. eval and exec take the namespaces explicitly (eval(expr, globals, locals)), so rows 7 and 8 mutate a dict the way Ruby mutates a binding, and the counter in row 8 is just a dict that exec("count += 1") updates.
The closest thing to a Binding is a frame object (row 9): inspect.currentframe() knows its code name and its locals, where the dict of row 1 knows neither. globals() is the module scope (row 10) — there is no main object, only __name__ == "__main__". A closure keeps its captured variables in cells (row 11, co_freevars and __closure__) rather than dragging a whole scope along as a Ruby Proc does. string.Template substitutes from any mapping (row 12), and an unknown name in eval is a NameError (row 13). One thing the twin deliberately does not do is read locals() a second time after writing to it: Python 3.12 returned the frame's single dict, so a write was visible to the next locals() call, while 3.13 (PEP 667) returns a fresh snapshot on every call and makes frame.f_locals a write-through proxy — the two CI runners would print different things. (Not machine-checked here.)
Verified output of binding_and_eval_py.py — regenerated by tools/run_examples.py, never hand-typed.
1. env = two_locals(); type(env).__name__, sorted(env) ['dict', ['a', 'b']]
2. env["a"] 1
3. eval("a + b", {}, env) 3
4. after env["a"] = 10: eval("a * b", {}, env) 20
5. env["c"] = 3 adds c to the dict [True, ['a', 'b', 'c']]
6. in a function, set through a locals() snapshot 'NameError'
7. exec("w = 7", {}, env); env["w"] 7
8. exec("count += 1", {}, ce) three times; count 3
9. a frame: f_code.co_name, its f_locals (a dict knows neither) ['frame_demo', ['a', 'b']]
10. globals()["__name__"], eval("z") ['__main__', 5]
11. a lambda's closure: co_freevars, cell contents [('z',), [5]]
12. string.Template("a is $a, b is $b").substitute(env) 'a is 10, b is 2'
13. eval("nope", {}, env) raises 'NameError'
Try it¶
- Write
render(template, **vars)that evaluates an ERB template in a clean binding holding only the variables passed in: takebindinginside an otherwise empty method,local_variable_seteach pair, then callresult. Show that a template naming a local of the caller raisesNameErrorthroughrenderbut works with the caller's ownbinding. - A binding is a closure. Write
make_counterthat returns a binding whosecountstarts at 0, then build two lambdas from it witheval("-> { count += 1 }", b)and show that calling either one advances the samecount— and thatb.local_variable_get(:count)sees every increment.
Solution to 1
binding_and_eval_kata_rb.rb in full — pasted here by tools/run_examples.py from the file CI runs.
# Kata: render an ERB template in a clean Binding that holds only the
# variables you pass in -- so a template cannot reach the caller's locals.
require "erb"
def clean_binding
binding
end
def render(template, **vars)
b = clean_binding
vars.each { |name, value| b.local_variable_set(name, value) }
ERB.new(template).result(b)
end
secret = "s3cret"
puts "1. render with greeting: and name: -> " + render("<%= greeting %>, <%= name %>!", greeting: "Hello", name: "Ada").inspect
puts "2. the clean binding sees no locals -> " + clean_binding.local_variables.inspect
begin
render("<%= secret %>")
rescue NameError => e
puts "3. render(\"<%= secret %>\") -> #{e.class}"
end
puts "4. the caller's binding sees secret -> " + ERB.new("<%= secret %>").result(binding).inspect
Verified output of binding_and_eval_kata_rb.rb — regenerated by tools/run_examples.py, never hand-typed.
See also¶
- A method is a value once you ask for it — the other half of a method made into an object: its code, receiver and owner
- TracePoint sees every call and return — a traced event carries
binding, the scope of the frame it happened in instance_evalandclass_eval— evaluating with a differentselfrather than a different set of locals- Introspection —
local_variablesanddefined?outside a binding - Closures capture variables — why every
Prochas abindingat all - String formatting and interpolation —
"#{}"for one string, ERB for a template - Scope is about names, not values ↗ — the Rust library separating a name's scope from its value's lifetime, which a
Bindingdeliberately ties back together Binding↗ andERB↗ — the reference forlocal_variable_*,receiverandresultevalandexec↗ — Python's explicit namespaces, and the note thatlocals()is a snapshotirbechoes every value; a script does not —irbis a read–eval–print loop: each line's value is printed withinspect,_holds the last one, an…