Tcl Part I · Module 7 · Dictionaries and Arrays
Two Ways to Hold Key-Value Data
Lists got you a long way, and they run out the moment a record grows past two or three fields — nobody wants to remember that position 4 is the timeout. Tcl answers that with two structures, not one, and most tutorials introduce them as interchangeable alternatives. They are not. One of them is a value, like a number or a string. The other is not a value at all; it is a family of variables sharing a name. This chapter teaches that single difference first, because it is what decides, in every later chapter, which structure your code is actually allowed to use.
Foundation11 min readTclPart IDictionariesArraysData structures
Part I · Module 7 · Chapter 7.1 · Two Ways to Hold Key-Value Data
1. The problem both structures solve
Module 6 ended with lassign, which turns a positional record into named variables:
lassign $row name width directionThat works, and it has a ceiling. The list still has to be built in exactly the right order, every producer and every consumer has to agree on that order, and adding a field in the middle quietly breaks everything downstream. Chapter 6.14 made that argument and then deferred the fix. This is the fix.
What you want is to store a value under a name rather than at a position:
# instead of: position 2 is the width
# you want: the key "width" is the widthTcl gives you two ways to do that. Both are correct Tcl. They behave completely differently.
2. The two forms, side by side
Here is the same data, held both ways:
# Form one — a dictionary
set cfg [dict create mode fast runs 3]
# Form two — an array
array set opt {mode fast runs 3}They look parallel. Read the next two lines and they stop looking parallel:
puts $cfg
# mode fast runs 3
puts $optcan't read "opt": variable is arrayThe first line printed. The second one is an error — and not a subtle one. cfg is a variable holding a value, so $cfg reads it. opt is not a variable at all. There is no such thing as "the value of opt".
3. What an array actually is
When you write this:
array set opt {mode fast runs 3}Tcl creates two variables. Their names are opt(mode) and opt(runs). The parentheses are part of the name, the same way the letters are. opt on its own is a label for the family, not a member of it.
puts $opt(mode)
puts $opt(runs)fast
3Those two lines work because each one names a real variable. This is why $opt fails: you asked Tcl to read a variable whose name is opt, and no such variable exists.
4. What a dictionary actually is
A dictionary is a value — in exactly the sense Chapter 3.1 used. You can prove it:
set cfg [dict create mode fast runs 3]
puts [expr {$cfg eq "mode fast runs 3"}]1The dictionary is the string mode fast runs 3. Not a wrapper around it, not something that prints like it — it is equal to it. A dict is a flat sequence of alternating keys and values, read by dictionary rules, in the same way a list is a string read by list rules.
So the familiar list commands still see it as text:
puts [llength $cfg]
puts [dict size $cfg]4
2Four words; two pairs. Same value, two readings — the same pattern Chapter 6.1 opened with.
5. The consequence that decides everything
Because a dict is a value and an array is not, they behave differently at every boundary where values move. A procedure boundary is the clearest one.
6. Proving it at a procedure boundary
A dict passes like any other argument:
proc summarise {d} {
return "[dict get $d mode] with [dict get $d runs] runs"
}
set cfg [dict create mode fast runs 3]
puts [summarise $cfg]fast with 3 runsNothing special happened there. $cfg is a value; values are what arguments are.
Now try the same thing with an array:
array set opt {mode fast runs 3}
proc summarise2 {a} {
return "got: $a"
}
puts [summarise2 $opt]can't read "opt": variable is arrayThe call never even reached the procedure. $opt failed while Tcl was assembling the arguments — exactly as it did in section 2. There is no value to hand over.
The way to give a procedure access to an array is to pass its name and link to it:
proc summarise3 {name} {
upvar 1 $name a
return "[set a(mode)] with [set a(runs)] runs"
}
puts [summarise3 opt]fast with 3 runsNotice what changed: you passed opt, not $opt. The procedure received a name and used upvar to bind it to a local alias. That works, and it is a different style of programming — the procedure now reaches into its caller's scope rather than receiving data.
7. Returning one
The same asymmetry runs the other way. A procedure can build a dict and return it:
proc make_config {} {
return [dict create mode fast runs 3]
}
set cfg [make_config]
puts [dict get $cfg mode]fastA procedure cannot return an array, because there is nothing to return. The closest it can do is flatten the array into a list and hand that back:
proc make_opts {} {
array set x {mode fast runs 3}
return [array get x]
}
puts [make_opts]runs 3 mode fastRead that output carefully: it is the same flat key-value text a dict is. Flattening an array produces something a dict can consume. That is the bridge, and Chapter 7.12 is built on it.
One detail to file away: the pairs came back as runs then mode, which is not the order they were written in. That is not a mistake and not a one-off — an array simply does not keep an order. Chapter 7.10 shows how to impose one, and Chapter 7.12 shows what it costs when you convert.
8. Nesting
One more consequence, and it is the one that bites hardest in real scripts. Because a dict is a value, you can put dicts inside lists and inside other dicts:
set rows [list [dict create n u1 w 8] [dict create n u2 w 16]]
foreach r $rows {
puts "[dict get $r n] is [dict get $r w] bits"
}u1 is 8 bits
u2 is 16 bitsA list of dictionaries. Completely ordinary. There is no equivalent for arrays — you cannot put an array inside a list, because an array is not a thing that can be inside anything. Chapter 7.6 builds on this properly.
9. So which one should you use?
The rule, stated once and applied for the rest of the module:
Use a dictionary when the data is something you pass, return, or store inside another structure. Use an array when you want variable-style access to a lot of named values inside a single scope — or when existing code hands you one.
In new code, the dictionary is the default. Reach for an array when you have a positive reason:
| Situation | Reach for |
|---|---|
| A procedure takes or returns the data | Dictionary |
| The data goes inside a list, or inside another record | Dictionary |
| You want insertion order preserved | Dictionary |
Counting things in one procedure with incr opt(x) | Array |
Tcl itself gives you one (env, tcl_platform) | Array — no choice |
| Existing code in your project uses them | Array — match the code |
That last row is not a small matter. Arrays predate dictionaries by well over a decade, and a great deal of working Tcl is written with them. Chapter 7.8 onwards teaches them properly, not as a historical curiosity.
10. Exercises
Exercise 1. Without running it, say which of these two lines errors and what the message will be:
set a [dict create x 1] ; puts $a
array set b {x 1} ; puts $bExercise 2. Write a procedure widest that takes a list of dictionaries, each with keys n and w, and returns the n of the one with the largest w.
Exercise 3. Explain in one sentence why summarise3 opt (section 6) passes opt rather than $opt.
Exercise 4. A colleague's procedure ends with return [array get results]. What does the caller receive, and what are the two things it can do with it?
Exercise 5. Show that a dictionary is a string by printing the result of comparing one with eq against the text you expect.
Exercise 6. For each of these, say dict or array and give your reason in half a sentence: (a) a count of pass and fail results tallied inside one procedure; (b) a record describing one module, returned by a procedure; (c) the environment variables Tcl gives you; (d) a list of twenty such records.
Answers
1. The first prints x 1. The second errors with can't read "b": variable is array — b is not a variable, only b(x) is.
2.
proc widest {rows} {
set best ""
set bestw -1
foreach r $rows {
if {[dict get $r w] > $bestw} {
set bestw [dict get $r w]
set best [dict get $r n]
}
}
return $best
}
puts [widest [list [dict create n u1 w 8] [dict create n u2 w 16]]]u23. Because $opt is an error — the procedure needs the array's name so that upvar can link to it, not its value, which does not exist.
4. A flat list of alternating keys and values. It can read it as a dictionary directly, or load it into an array of its own with array set.
5.
set d [dict create mode fast runs 3]
puts [expr {$d eq "mode fast runs 3"}]16. (a) array — local tallying with incr is exactly what arrays are good at; (b) dict — it is returned, so it must be a value; (c) array — env is one, you have no choice; (d) dict — only values can go inside a list.
11. Remember
- A dictionary is a value.
dict createreturns one;$cfgreads it; it is literally equal to its own flat text. - An array is a family of variables.
array setcreates them;$optalone is an error —can't read "opt": variable is array. - Only values cross procedure boundaries. A dict passes and returns normally; an array needs
upvarand a name. - Only values nest. Dicts go inside lists and inside dicts. Arrays go nowhere.
array getflattens an array into exactly the shape a dict is — that is the bridge between them.- Default to a dictionary in new code; use an array for scope-local tallying, or when Tcl or existing code hands you one.
llengthanddict sizeread the same value differently — four words, two pairs.
Where this fits
Part of the Tcl curriculum.
