Tcl Part I · Module 7 · Dictionaries and Arrays
Converting Between Arrays and Dictionaries
You will not often get to pick one structure and keep it. Existing code hands you arrays, new code wants dictionaries, and the two have to meet somewhere. Happily the bridge is two commands you already know, because array get produces exactly the shape a dictionary is. This closing chapter shows the conversion in both directions, confirms what survives it — more than you might expect, including nested structure — and is precise about the single thing it destroys, which is not reversible and not reported.
Intermediate11 min readTclPart IDictionariesArraysMigration
Part I · Module 7 · Chapter 7.12 · Converting Between Arrays and Dictionaries
1. The bridge is a shape, not a command
There is no array to dict command, and none is needed. array get returns a flat list of alternating names and values — which is precisely what a dictionary is:
array set limits {retries 3 timeout 60 mode fast}
set d [array get limits]
puts "as text : $d"
puts "as a dict : [dict size $d] pairs, retries = [dict get $d retries]"as text : mode fast retries 3 timeout 60
as a dict : 3 pairs, retries = 3No conversion happened. array get produced a string, and dict get read that string by dictionary rules — the point Chapter 7.1 §4 made about a dictionary being its own text.
The other direction is array set:
set cfg [dict create mode fast runs 3]
array set opt $cfg
puts "opt(mode) = $opt(mode)"opt(mode) = fast2. What survives: more than you would guess
Keys containing spaces:
set sp [dict create "max fanout" 16 ok 1]
array set a2 $sp
puts "names : [array names a2]"
puts "read : $a2(max fanout)"
puts "back : [array get a2]"names : ok {max fanout}
read : 16
back : ok 1 {max fanout} 16The automatic quoting from Chapter 6.2 carries the key through intact. Note that $a2(max fanout) reads fine unquoted — substitution takes everything up to the closing parenthesis as the index, so only the literal set form needed grouping back in Chapter 7.8 §3.
Values containing spaces:
set vs [dict create note "two words" n 1]
array set a3 $vs
puts "element: '$a3(note)'"
puts "back : [array get a3]"element: 'two words'
back : n 1 note {two words}Nested dictionaries, which is the surprising one:
set nest [dict create outer [dict create inner 1] flat 2]
array set a4 $nest
puts "a4(outer) : '$a4(outer)'"
puts "usable : [dict get $a4(outer) inner]"
puts "back : [array get a4]"
puts "deep get : [dict get [array get a4] outer inner]"a4(outer) : 'inner 1'
usable : 1
back : flat 2 outer {inner 1}
deep get : 1The nested dictionary became the string value of one element, and because it is still a well-formed dictionary string, dict get reads straight through it — even after converting back. The array never understood the nesting; it did not need to.
Empty converts both ways:
array set a6 [dict create]
puts "dict->array: exists [array exists a6], size [array size a6]"
array set a7 {}
puts "array->dict: '[array get a7]', size [dict size [array get a7]]"dict->array: exists 1, size 0
array->dict: '', size 03. What does not survive: order
set ordered [dict create z 1 y 2 x 3 w 4 v 5]
puts "original : [dict keys $ordered]"
array set tmp $ordered
puts "via array : [array names tmp]"
puts "converted back: [dict keys [array get tmp]]"original : z y x w v
via array : x y v z w
converted back: x y v z wThe dictionary knew its keys were z y x w v. The array does not keep an order (Chapter 7.10 §4), so the moment the data passed through it, that information was gone — and converting back does not bring it back, because there is nothing left to bring back.
If the order matters, carry it separately:
set ordered [dict create z 1 y 2 x 3]
set key_order [dict keys $ordered]
array set tmp $ordered
# ... array-based code does its work ...
set rebuilt [dict create]
foreach k $key_order {
if {[info exists tmp($k)]} {
dict set rebuilt $k $tmp($k)
}
}
puts [dict keys $rebuilt]z y xOr, more simply: sort. An arbitrary order is unacceptable in output, and alphabetical is at least reproducible.
4. The merge hazard
Converting a dictionary into an array uses array set, and array set merges (Chapter 7.9 §3). Converting into an array that already has contents does not replace them:
array set a5 {old 1}
array set a5 [dict create new 2]
puts [lsort [array names a5]]new oldold is still there. In a loop that converts one record at a time into the same array, every record inherits every previous record's leftovers:
set records [list [dict create n u1 w 8] [dict create n u2]]
foreach r $records {
array unset rec
array set rec $r
puts "[array get rec]"
}n u1 w 8
n u2The array unset rec is what makes the second record correct. Without it, u2 would appear to have a width of 8 inherited from u1 — a result that is plausible, wrong, and very hard to spot.
5. The practical pattern: a seam
The reason to convert is usually to put a boundary between old and new code. The clean shape is to convert at the edge and work in one form inside:
# Old code: fills a caller's array
proc legacy_read_config {name} {
upvar 1 $name cfg
array set cfg {mode fast retries 3 timeout 60}
}
# New code: takes and returns a dictionary
proc apply_policy {cfg} {
if {[dict get $cfg mode] eq "fast"} {
dict set cfg timeout [expr {[dict get $cfg timeout] / 2}]
}
return $cfg
}
# The seam
array unset raw
legacy_read_config raw
set cfg [apply_policy [array get raw]]
foreach k [lsort [dict keys $cfg]] {
puts [format " %-8s %s" $k [dict get $cfg $k]]
} mode fast
retries 3
timeout 30One conversion, at one line, in one direction. The new procedure is a function on values and knows nothing about arrays; the old one is untouched. That is what migrating a boundary at a time looks like, as Chapter 7.11 §7 recommended.
And the other direction, when a new-style result must go back to old-style code:
proc legacy_report {name} {
upvar 1 $name a
foreach k [lsort [array names a]] { puts " $k = $a($k)" }
}
array unset out
array set out $cfg
legacy_report out mode = fast
retries = 3
timeout = 306. A quick reference
| Direction | Command | Notes |
|---|---|---|
| array → dict | set d [array get a] | always valid; order is arbitrary |
| array → dict, filtered | set d [array get a pat] | glob on element names |
| dict → array | array set a $d | merges; array unset a first |
| dict → array, fresh | array unset a ; array set a $d | the safe form |
| test before converting | array exists a | not info exists a |
7. Exercises
Exercise 1. Why does dict get [array get limits] retries work with no conversion step?
Exercise 2. After this, what is [lsort [array names a]], and why is it not just x?
array set a {old 1}
array set a [dict create x 2]Exercise 3. A dictionary built as z y x comes back from a round trip through an array as something else. What exactly was lost, and can it be recovered from the result?
Exercise 4. Write a loop over three records that converts each into the same array and prints it, without any record inheriting the previous one's keys.
Exercise 5. Write array_to_sorted_dict — returns a dictionary built from an array with its keys in alphabetical order, so that the result at least has a reproducible order.
Exercise 6. Show that a nested dictionary survives a round trip through an array, by reading a two-level value out of the converted result.
Answers
1. Because array get returns a flat list of alternating names and values, and that is exactly what a dictionary is. There is nothing to convert — dict get simply reads the same string by dictionary rules.
2. old x. array set merges into whatever is already there rather than replacing it, so old survives.
3. The order of the keys. It cannot be recovered from the result, because an array never stored it — the converted dictionary is complete and correct in every other respect. The only fix is to have kept dict keys beforehand, or to impose a fresh order such as lsort.
4.
set records [list \
[dict create n u1 w 8] \
[dict create n u2] \
[dict create n u3 w 32 d 4]]
foreach r $records {
array unset rec
array set rec $r
puts "[lsort [array names rec]]"
}n w
n
d n wThe second record has only n, which is the point — without array unset it would have shown n w.
5.
proc array_to_sorted_dict {name} {
upvar 1 $name a
set d [dict create]
foreach k [lsort [array names a]] {
dict set d $k $a($k)
}
return $d
}
array set lim {retries 3 timeout 60 mode fast}
puts [array_to_sorted_dict lim]mode fast retries 3 timeout 60Because a dictionary preserves insertion order, inserting in sorted order makes the result's order both defined and alphabetical.
6.
set nest [dict create outer [dict create inner 1] flat 2]
array set a4 $nest
puts [dict get [array get a4] outer inner]1The nested dictionary rode through as the string value of one element, and it is still a valid dictionary string on the way out.
8. What Module 7 covered
Twelve chapters, two structures, one rule applied throughout:
- The difference between a value and a family of variables, and why it decides everything
- Dictionaries:
dict create,set,get,exists,keys,values,size,for, nesting with key paths, andwith/update - Arrays: elements as variables,
array set/get/names/exists/size/unset, andparray - The ordering guarantee one has and the other does not
- Choosing between them on four axes, with the performance question measured rather than assumed
- Converting between them, and the one thing conversion destroys
The thread is the one Chapter 7.1 started with: a dictionary is a value, so it passes, returns, nests and keeps its order; an array is a set of variables, so it does none of those and in exchange gives you ordinary variable syntax on names computed at run time. Every difference in the module follows from that single fact.
9. What comes next
Module 8 is Conditions and Control Flow. This module has leaned on if in almost every chapter without ever teaching it properly — the braces around the condition, the braces around the body, and why both are mandatory rather than stylistic. Module 8 settles that, then adds switch with its three matching modes and its fallthrough rule.
It also closes a loop opened back in Chapter 3.6: a value that is neither true nor false is an error in a condition, not a quiet false — and the honest test for a value that is missing rather than wrong is exactly the dict exists and info exists discipline you have just spent twelve chapters practising.
10. Remember
array getproduces the shape a dictionary is, so no conversion command is needed in that direction.array set a $dictgoes the other way — and it merges, soarray unset afirst whenever the array may not be empty.- Reusing one array in a loop without
array unsetis the classic bug: records silently inherit the previous record's keys. - Keys and values containing spaces survive the trip, thanks to automatic quoting.
- Nested dictionaries survive as string values, and remain deep-readable after converting back.
- Order does not survive. It is destroyed silently, one way, and cannot be recovered from the result.
- If order matters, save
dict keysfirst or imposelsorton the way out. - Convert at a seam — one line, one direction — and work in a single form on each side of it.
Where this fits
Part of the Tcl curriculum.
