Tcl Part I · Module 7 · Dictionaries and Arrays
dict keys, dict values, dict size
Three small commands that tell you what is in a dictionary without reading it key by key. They are easy, and two things about them are worth more than the syntax: they each accept a glob pattern, which turns a four-line loop into one line and which most Tcl programmers never discover; and the way you count a dictionary is not the way you count a list, even though a dictionary is one. This chapter settles both, and it reuses the pattern language you already learned twice in earlier modules.
Foundation10 min readTclPart IDictionariesIntrospectionGlob patterns
Part I · Module 7 · Chapter 7.4 · dict keys, dict values, dict size
1. The three commands
set cfg [dict create max_fanout 16 max_tran 0.4 mode fast runs 3]
puts [dict keys $cfg]
puts [dict values $cfg]
puts [dict size $cfg]max_fanout max_tran mode runs
16 0.4 fast 3
4dict keys and dict values each return a list, which means everything from Module 6 applies to the result — lsort, lsearch, foreach, llength. dict size returns the number of pairs.
All three take a value, not a variable name. None of them modifies anything.
2. The order is the insertion order
set d [dict create]
foreach k {zebra apple mango} {
dict set d $k 1
}
puts [dict keys $d]zebra apple mangoNot alphabetical — the order you put them in. Chapter 7.2 §3 established this guarantee, and here is where you actually use it: if you built a dictionary in a meaningful order, dict keys hands it back in that order, and a report built from it comes out the way you intended.
If you want alphabetical, ask for it:
puts [lsort [dict keys $d]]apple mango zebra3. The pattern argument
Each of dict keys and dict values takes an optional glob pattern:
set cfg [dict create max_fanout 16 max_tran 0.4 mode fast runs 3]
puts [dict keys $cfg max_*]max_fanout max_tranThat is the same pattern language as string match from Chapter 5.5 and the default mode of lsearch from Chapter 6.9: * for any run of characters, ? for exactly one, [...] for a character set. Nothing new to learn.
puts [dict keys $cfg *a*]
puts [dict keys $cfg mode]
puts "[dict keys $cfg zzz*]<- nothing matched"max_fanout max_tran
mode
<- nothing matchedNo match gives an empty list, not an error — the same behaviour lsearch has.
Compare the one-liner with the loop it replaces:
# the long way
set found {}
foreach k [dict keys $cfg] {
if {[string match max_* $k]} { lappend found $k }
}
puts $found
# the short way
puts [dict keys $cfg max_*]max_fanout max_tran
max_fanout max_tran4. dict values filters on the value
This is the one that misleads people:
puts [dict values $cfg f*]fastThe pattern applied to the values, not the keys. fast is a value; mode is its key. It is consistent — dict keys patterns keys, dict values patterns values — and it reads wrong at a glance because you are usually thinking in keys.
5. Counting: pairs versus words
Chapter 7.1 showed that a dictionary is literally its own flat text. So there are two defensible answers to "how big is it?", and they differ:
set cfg [dict create mode fast runs 3]
puts "llength : [llength $cfg]"
puts "dict size : [dict size $cfg]"llength : 4
dict size : 2Four words; two pairs. dict size is almost always the one you want, and llength on a raw dictionary is almost always a bug — it is the dictionary version of the string length versus llength contrast that opened Chapter 6.1.
Counting the result of dict keys is fine, because that really is a list:
puts [llength [dict keys $cfg]]2And it stays correct even when values contain spaces, because dict values quotes properly — the automatic quoting from Chapter 6.2:
set sp [dict create a "two words" b x]
puts "values : [dict values $sp]"
puts "llength : [llength [dict values $sp]]"
puts "dict size : [dict size $sp]"values : {two words} x
llength : 2
dict size : 2Both say 2. The raw text would not:
puts [llength $sp]46. Emptiness
set e [dict create]
puts "size : [dict size $e]"
puts "keys : '[dict keys $e]'"
puts "empty? : [expr {[dict size $e] == 0}]"size : 0
keys : ''
empty? : 1dict size is the clear test. Comparing the dictionary to "" also happens to work, because an empty dictionary is the empty string — but it reads as a string comparison and will confuse the next person.
7. Putting them together
A tidy settings report, sorted, aligned, and filtered:
set cfg [dict create max_fanout 16 max_tran 0.4 mode fast runs 3 width 8]
puts "[dict size $cfg] settings"
foreach k [lsort [dict keys $cfg]] {
puts [format " %-12s %s" $k [dict get $cfg $k]]
}
puts "limits only:"
foreach k [lsort [dict keys $cfg max_*]] {
puts [format " %-12s %s" $k [dict get $cfg $k]]
}5 settings
max_fanout 16
max_tran 0.4
mode fast
runs 3
width 8
limits only:
max_fanout 16
max_tran 0.4format with %-12s is from Chapter 5.11. The whole report is four commands you already knew and one you just met.
The next chapter shows a shorter way to write that loop — one that hands you the key and the value together, without a dict get inside the body.
8. Exercises
Exercise 1. Given set d [dict create b 2 a 1 c 3], what do dict keys $d and lsort [dict keys $d] print?
Exercise 2. Write one line that prints how many keys in $cfg begin with max_.
Exercise 3. Explain why llength $cfg and dict size $cfg disagree, and which one is nearly always the bug.
Exercise 4. What does dict values $cfg 1* return for set cfg [dict create a 16 b 0.4 c 100], and why is the answer about values rather than keys?
Exercise 5. Write a procedure summary that returns a one-line string like 3 keys: a, b, c with the keys in alphabetical order.
Exercise 6. Someone writes if {$cfg eq ""} { puts "empty" } to test for an empty dictionary. It works. Give the better test and one reason to prefer it.
Answers
1. b a c — insertion order. Then a b c.
2.
set cfg [dict create max_fanout 16 max_tran 0.4 mode fast]
puts [llength [dict keys $cfg max_*]]23. llength reads the dictionary as a flat list of words, so it counts keys and values; dict size counts pairs. llength on a raw dictionary is nearly always the bug — it reports double the real size whenever every value is a single word, and something else entirely when they are not.
4.
set cfg [dict create a 16 b 0.4 c 100]
puts [dict values $cfg 1*]16 100dict values matches the pattern against the values, so 16 and 100 qualify and 0.4 does not. The keys are never consulted.
5.
proc summary {d} {
set ks [lsort [dict keys $d]]
return "[llength $ks] keys: [join $ks {, }]"
}
puts [summary [dict create c 3 a 1 b 2]]3 keys: a, b, c6. if {[dict size $cfg] == 0}. It says what it means — an empty dictionary is a size question, not a string question — and it does not quietly rely on the coincidence that the empty dictionary and the empty string are the same text.
9. Remember
dict keysanddict valuesreturn lists, so every list command from Module 6 works on the result.- The order is insertion order, guaranteed. Use
lsortwhen you want alphabetical. - Both take an optional glob pattern — the same language as
string matchandlsearch. No match gives an empty list. dict values $d patmatches the values, not the keys of those values. To get values for matching keys, pattern the keys and look each one up.dict sizecounts pairs;llengthon a raw dictionary counts words and is nearly always wrong.llength [dict keys $d]is fine — that really is a list.- Test emptiness with
dict size, not by comparing to"".
Where this fits
Part of the Tcl curriculum.
