Tcl Part I · Module 7 · Dictionaries and Arrays
dict for: Iterating Key and Value Together
The last chapter ended with a loop over the keys that had to look each value up again inside the body. That works and it is one step longer than it needs to be. dict for hands you both halves of every pair at once, in the order you built the dictionary, and it reads exactly like what it does. Three things about it are worth knowing before you rely on it: it insists on exactly two variable names, it takes a value where you may expect a name, and it walks a snapshot rather than a live view — which is the difference between a loop that terminates and one that does not.
Foundation11 min readTclPart IDictionariesdict forIteration
Part I · Module 7 · Chapter 7.5 · dict for: Iterating Key and Value Together
1. The shape
set cfg [dict create mode fast runs 3 width 8]
dict for {key value} $cfg {
puts "$key = $value"
}mode = fast
runs = 3
width = 8Three parts: a two-element list of variable names, the dictionary, and a body. On each pass Tcl sets both variables and runs the body.
Compare it with the version from Chapter 7.4 §7:
# the two-step way
foreach key [dict keys $cfg] {
puts "$key = [dict get $cfg $key]"
}mode = fast
runs = 3
width = 8Same output. dict for says it in one step, and it does not look the key up a second time.
2. Exactly two names
Not one, not three:
dict for {key} $cfg { puts $key }must have exactly two variable namesdict for {key value extra} $cfg { puts $key }must have exactly two variable namesThis differs from foreach, which happily takes any number of variables and strides through the list in groups. dict for is about pairs, so it is always two.
3. It takes a value
Like dict get and unlike dict set, the second argument is the dictionary itself:
dict for {k v} $cfg { ... } ;# correct — a value
dict for {k v} cfg { ... } ;# wrong — a nameThe wrong form produces a message that does not obviously point at the mistake:
set cfg [dict create mode fast]
dict for {k v} cfg { puts $k }missing value to go with keyThat reads like a complaint about your data. It is not. Tcl took the literal word cfg as the dictionary, and a single word is an odd number of elements, so it is not a valid dictionary. The error is correct and the cause is the missing $.
4. The order is guaranteed
set d [dict create]
foreach k {zebra apple mango} {
dict set d $k 1
}
dict for {k v} $d { puts -nonewline "$k " }
puts ""zebra apple mangoInsertion order, as Chapter 7.2 promised. This is worth more than it sounds: it means a report generated by dict for comes out in an order you controlled, and it means two runs over the same dictionary produce identical output. Neither is true of arrays — Chapter 7.10 shows the contrast.
5. break, continue and return
All three behave the way they do in foreach:
set d [dict create a 1 b 2 c 3 d 4]
dict for {k v} $d {
if {$v == 3} break
puts "saw $k"
}saw a
saw bdict for {k v} $d {
if {$v % 2} continue
puts "even: $k = $v"
}even: b = 2
even: d = 4And return leaves the enclosing procedure, which makes a search read nicely:
proc key_for {d want} {
dict for {k v} $d {
if {$v eq $want} { return $k }
}
return ""
}
puts [key_for [dict create a 1 b 2 c 3] 2]
puts "[key_for [dict create a 1] 9]<- not found"b
<- not found6. It walks a snapshot
This is the behaviour worth knowing before you need it. dict for takes a value — so it iterates the dictionary as it was when the loop started, even if the variable changes underneath:
set live [dict create a 1 b 2]
dict for {k v} $live {
dict set live zzz 9
puts "iterating $k (size is now [dict size $live])"
}
puts "after: $live"iterating a (size is now 3)
iterating b (size is now 3)
after: a 1 b 2 zzz 9The loop ran exactly twice. The key added inside the body really was added — the final line proves it — and the loop never saw it. This is a direct consequence of dict for taking $live rather than live: the value was handed over once, at the start.
That is the behaviour you want. A loop that grew while you fed it would never finish.
7. The loop variables outlive the loop
set d [dict create a 1 b 2]
dict for {k v} $d { }
puts "k = $k, v = $v"k = b, v = 2They hold whatever the last iteration left. This is the same as foreach and it is occasionally useful, but relying on it makes code fragile — an empty dictionary leaves them untouched or unset, and the loop then silently reuses a stale value.
8. A worked example
A report with a total and an aligned layout:
set runs [dict create smoke 120 regress 4500 nightly 18000 lint 60]
set total 0
puts [format "%-10s %8s" "RUN" "CYCLES"]
dict for {name cycles} $runs {
puts [format "%-10s %8d" $name $cycles]
incr total $cycles
}
puts [format "%-10s %8d" "TOTAL" $total]RUN CYCLES
smoke 120
regress 4500
nightly 18000
lint 60
TOTAL 22680The rows came out in the order the dictionary was built, which is the order a reader expects. Add lsort on the keys if you want alphabetical instead — but then you are back to foreach and a dict get, which is a fair trade and worth seeing:
foreach name [lsort [dict keys $runs]] {
puts [format "%-10s %8d" $name [dict get $runs $name]]
}lint 60
nightly 18000
regress 4500
smoke 1209. Exercises
Exercise 1. What error does this produce, and why?
dict for {k} [dict create a 1] { puts $k }Exercise 2. Write a loop that prints only the pairs whose value is greater than 100, given set d [dict create a 5 b 500 c 50 d 5000].
Exercise 3. Explain why dict for {k v} cfg {...} gives missing value to go with key rather than something about variables.
Exercise 4. Predict the number of iterations:
set d [dict create a 1 b 2]
set n 0
dict for {k v} $d { dict set d new$n 1 ; incr n }
puts $nExercise 5. Write invert, which takes a dictionary and returns a new one with keys and values swapped. Say what happens if two keys share a value.
Exercise 6. Rewrite this with dict for and say what improves:
foreach k [dict keys $cfg] {
set v [dict get $cfg $k]
puts "$k -> $v"
}Answers
1. must have exactly two variable names. dict for iterates pairs, so the variable list is always two long — unlike foreach, which accepts any number.
2.
set d [dict create a 5 b 500 c 50 d 5000]
dict for {k v} $d {
if {$v > 100} { puts "$k = $v" }
}b = 500
d = 50003. Because the missing $ makes the literal word cfg the dictionary. One word is an odd number of elements, so it fails the dictionary-structure check before any key is ever looked at. The message is about the value you accidentally supplied.
4. 2. dict for was handed the value as it stood at the start, so the keys added during the loop are not visited — though they really are added to d.
5.
proc invert {d} {
set out [dict create]
dict for {k v} $d { dict set out $v $k }
return $out
}
puts [invert [dict create a 1 b 2]]
puts [invert [dict create a 1 b 1]]1 a 2 b
1 bIf two keys share a value, the later one wins and the earlier is lost — the inverted dictionary has fewer pairs than the original.
6.
dict for {k v} $cfg {
puts "$k -> $v"
}It drops a line, removes a second lookup of a key you already have, and states that you want pairs rather than making a reader infer it.
10. Remember
dict for {key value} $d {body}hands you both halves of each pair, in insertion order.- Exactly two variable names — anything else gives
must have exactly two variable names. - It takes a value, not a name. Forgetting the
$givesmissing value to go with key, which is about the word you passed, not your data. break,continueandreturnwork normally.- It iterates a snapshot — keys added during the loop are added but never visited, so the loop always terminates.
- To build a changed dictionary, write into a separate one. Clearer than mutating while looping.
- The loop variables survive the loop, holding the last pair. Do not build on that.
- Use
foreachoverlsort [dict keys $d]when you need an order other than insertion order.
Where this fits
Part of the Tcl curriculum.
