Skip to content
VLSI Mentor

Tcl Part I · Module 7 · Dictionaries and Arrays

Nested Dictionaries

Real configuration is not flat. A run has stages, each stage has settings, and some of those settings have settings of their own. Because a dictionary is a value, a dictionary can hold dictionaries, and Tcl gives you a way to reach straight through the whole structure in one command rather than unpacking a level at a time. This chapter teaches that key path, shows that intermediate levels are created for you, and then spends real time on the rule that catches people out: what you pull out of a nested dictionary is a copy, so changing it changes nothing until you put it back.

Intermediate12 min readTclPart IDictionariesNestingConfiguration

Part I · Module 7 · Chapter 7.6 · Nested Dictionaries

1. A dictionary can hold a dictionary

Nothing special is required. A dictionary value is a value, so it can be another dictionary:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
set parse [dict create timeout 60 retries 2]
set cfg [dict create stages [dict create parse $parse]]
puts $cfg
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
stages {parse {timeout 60 retries 2}}

Read the braces as structure. stages holds one pair; that pair's value, parse, holds two.

2. The key path

To read something two levels down, you do not need two calls. dict get takes as many keys as you like:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
puts [dict get $cfg stages parse timeout]
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
60

Each key after the first descends one level. Stopping early gives you the whole sub-dictionary:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
puts [dict get $cfg stages parse]
puts [dict get $cfg stages]
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
timeout 60 retries 2
parse {timeout 60 retries 2}

The long way works too, and there is no reason to write it:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
# avoid — same result, three commands
puts [dict get [dict get [dict get $cfg stages] parse] timeout]
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
60

3. dict set creates the levels for you

You do not have to build the structure before you fill it:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
set cfg [dict create]
dict set cfg stages parse timeout 60
dict set cfg stages parse retries 2
dict set cfg stages build timeout 300

puts $cfg
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
stages {parse {timeout 60 retries 2} build {timeout 300}}

cfg did not exist, stages did not exist, parse did not exist. dict set made all of them. The last argument is the value; everything between the variable name and it is a key path.

This goes as deep as you like:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
set deep [dict create]
dict set deep a b c d e 5
puts $deep
puts [dict get $deep a b c d e]
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
a {b {c {d {e 5}}}}
5

4. What you cannot do

You can create a level under a key that does not exist. You cannot create one under a key whose value is an ordinary scalar:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
set d [dict create k 1]
dict set d k sub 2
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
missing value to go with key

k holds 1. To put sub inside it, Tcl would have to read 1 as a dictionary, and a single word is an odd number of elements. Replace the value wholesale instead:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
set d [dict create k 1]
dict set d k [dict create sub 2]
puts $d
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
k {sub 2}

5. Testing and removing, at depth

dict exists takes a path too, and it is the safe way to probe:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
set cfg [dict create stages [dict create parse [dict create timeout 60]]]

puts [dict exists $cfg stages parse timeout]
puts [dict exists $cfg stages nope timeout]
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
1
0

The second returns 0 rather than erroring, because stages really is a dictionary — nope is simply not one of its keys. (Chapter 7.3 §6 covers the case where it does error.)

dict unset also takes a path, and removes only the leaf:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
set g [dict create s [dict create p 1 q 2]]
dict unset g s p
puts $g
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
s {q 2}

Removing a key that is not there is silently fine:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
set h [dict create a 1]
dict unset h zz
puts $h
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
a 1

And a deep dict get through a missing key is an error that names the key:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
set cfg [dict create stages [dict create parse [dict create timeout 60]]]
puts [dict get $cfg stages nope timeout]
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
key "nope" not known in dictionary

6. Iterating a nested structure

dict for gives you each sub-dictionary as the value, and you can loop again inside:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
set cfg [dict create \
    parse [dict create timeout 60  retries 2] \
    build [dict create timeout 300 retries 0]]

dict for {stage settings} $cfg {
    puts "$stage:"
    dict for {k v} $settings {
        puts [format "    %-8s %s" $k $v]
    }
}
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
parse:
    timeout  60
    retries  2
build:
    timeout  300
    retries  0

The backslashes continue the command across lines — Chapter 2.10.

7. A sub-dictionary is a copy

Here is the rule that decides whether your updates land. Everything in Tcl is a value, and values are copied, so what dict get hands you is not a window into the parent:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
set cfg [dict create stages [dict create parse [dict create timeout 60]]]

set p [dict get $cfg stages parse]
dict set p timeout 999

puts "my copy : $p"
puts "original: [dict get $cfg stages parse]"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
my copy : timeout 999
original: timeout 60

The original is untouched. This is exactly the behaviour Chapter 6.6 established for lists, applied one level down, and it is not a flaw — it is what makes dictionaries safe to pass around.

Two correct ways to actually change the nested value. The direct one:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
dict set cfg stages parse timeout 999
puts [dict get $cfg stages parse timeout]
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
999

Or pull out, modify, and put back:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
set p [dict get $cfg stages parse]
dict set p retries 5
dict set cfg stages parse $p
puts [dict get $cfg stages parse]
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
timeout 999 retries 5

8. A worked example

A nested configuration with defaults applied at depth, then a report:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
proc apply_default {cfgName stage key value} {
    upvar 1 $cfgName cfg
    if {![dict exists $cfg $stage $key]} {
        dict set cfg $stage $key $value
    }
}

set cfg [dict create]
dict set cfg parse timeout 60
dict set cfg build timeout 300

foreach stage [dict keys $cfg] {
    apply_default cfg $stage retries 1
    apply_default cfg $stage verbose 0
}

dict for {stage settings} $cfg {
    puts "$stage ([dict size $settings] settings)"
    foreach k [lsort [dict keys $settings]] {
        puts [format "    %-8s %s" $k [dict get $settings $k]]
    }
}
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
parse (3 settings)
    retries  1
    timeout  60
    verbose  0
build (3 settings)
    retries  1
    timeout  300
    verbose  0

Note the upvar in apply_default: the procedure has to modify the caller's dictionary, so it takes the name. A procedure that only reads would take $cfg. That is the same distinction as Chapter 7.1, now showing up in your own code.

9. Exercises

Exercise 1. What does this print?

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
set d [dict create]
dict set d a b c 1
puts $d

Exercise 2. Given the cfg from section 6, write one line that prints the build stage's timeout.

Exercise 3. Why does dict set d k sub 2 fail when d is k 1, but succeed when d is empty?

Exercise 4. Predict the output and explain it:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
set c [dict create s [dict create t 1]]
set inner [dict get $c s]
dict set inner t 99
puts [dict get $c s t]

Exercise 5. Write deep_get — a procedure taking a dictionary, a list of keys, and a default, returning the value at that path or the default if any step is missing. It must not raise an error.

Exercise 6. Build a two-level dictionary of three modules, each with width and depth, then print a table with a row per module.

Answers

1. a {b {c 1}}. Both intermediate levels were created automatically.

2.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
puts [dict get $cfg build timeout]
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
300

3. When d is empty there is nothing in the way — dict set creates k as a dictionary. When k already holds 1, Tcl must read that value as a dictionary to put sub inside it, and a single word is an odd-length dictionary.

4. It prints 1. dict get returned a copy, so dict set inner t 99 changed only the copy. The original is untouched.

5.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
proc deep_get {d path {default ""}} {
    foreach k $path {
        if {[catch {dict exists $d $k} ok] || !$ok} { return $default }
        set d [dict get $d $k]
    }
    return $d
}

set c [dict create s [dict create t 1]]
puts [deep_get $c {s t} none]
puts [deep_get $c {s zz} none]
puts [deep_get $c {zz t} none]
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
1
none
none

The catch around dict exists matters: one step at a time means the current d may be a scalar, and dict exists errors on that rather than returning 0 — exactly the case from Chapter 7.3 §6.

6.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
set mods [dict create]
dict set mods fifo  width 8  
dict set mods fifo  depth 16
dict set mods cache width 32
dict set mods cache depth 256
dict set mods regs  width 16
dict set mods regs  depth 4

puts [format "%-8s %6s %6s" MODULE WIDTH DEPTH]
dict for {name m} $mods {
    puts [format "%-8s %6d %6d" $name [dict get $m width] [dict get $m depth]]
}
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
MODULE    WIDTH  DEPTH
fifo          8     16
cache        32    256
regs         16      4

10. Remember

  • A dictionary can hold dictionaries, because a dictionary is a value.
  • dict get $d k1 k2 k3 descends a key path in one call. Stopping early returns the sub-dictionary.
  • dict set name k1 k2 key value creates every intermediate level — which also means a typo silently builds a new branch.
  • You cannot graft a level onto a scalar value — missing value to go with key. Replace the value with a dictionary instead.
  • dict exists and dict unset take paths too. dict unset removes only the leaf, and is silent about keys that were never there.
  • A deep dict get through a missing key names it: key "nope" not known in dictionary.
  • What dict get returns is a copy. Modifying a retrieved sub-dictionary does not touch the parent — use the key path, or put it back explicitly.
  • A procedure that modifies a dictionary needs the name and upvar; one that only reads takes the value.

Where this fits

Part of the Tcl curriculum.