Skip to content
VLSI Mentor

Tcl Part I · Module 7 · Dictionaries and Arrays

dict with and dict update

These two commands let you stop writing dict get and dict set for a while and work on a dictionary's contents as ordinary variables. That is genuinely pleasant for a block of arithmetic, and one of the two does something you need to know about before you use it: it creates a local variable for every key, overwriting anything you already had by that name, and it does not put it back afterwards. This chapter shows both commands completely, including the write-back rules, which are not what most people assume — and then gives a straight recommendation about which to reach for.

Intermediate13 min readTclPart IDictionariesdict withdict update

Part I · Module 7 · Chapter 7.7 · dict with and dict update

1. The idea

When a block of code reads and writes several keys of the same dictionary, dict get and dict set get noisy:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
set cfg [dict create width 8 depth 16]
dict set cfg total [expr {[dict get $cfg width] * [dict get $cfg depth]}]
puts [dict get $cfg total]
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
128

dict with lets the same block use plain variables:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
set cfg [dict create width 8 depth 16]
dict with cfg {
    set total [expr {$width * $depth}]
}
puts $cfg
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
width 8 depth 16

Look closely at that output — total is not there. That is the first rule, and section 5 explains it. Set an existing key instead and the change does land:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
set cfg [dict create width 8 depth 16 total 0]
dict with cfg {
    set total [expr {$width * $depth}]
}
puts $cfg
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
width 8 depth 16 total 128

2. The shape of both commands

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
dict with dictVariable ?key ...? {body}
dict update dictVariable key varName ?key varName ...? {body}

dict with makes a variable for every key. dict update makes variables only for the keys you name, and lets you choose the variable names:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
set cfg [dict create mode fast runs 3 width 8]

dict update cfg runs r width w {
    incr r 10
    set w 64
}
puts $cfg
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
mode fast runs 13 width 64

Both take the dictionary variable name, bare — they modify it. That is the rule from Chapter 7.2 §7.

3. The trap: dict with overwrites your locals

This is the one to remember from the whole chapter.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
proc demo {} {
    set runs 999
    set mode "careful"

    set c [dict create runs 3]

    puts "before: runs=$runs mode=$mode"
    dict with c {
        puts "inside: runs=$runs mode=$mode"
    }
    puts "after : runs=$runs mode=$mode"
}
demo
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
before: runs=999 mode=careful
inside: runs=3 mode=careful
after : runs=3 mode=careful

Read the first and last lines. runs was 999 before the block and is 3 after it. The dictionary had a key called runs, so dict with created a local variable of that name — destroying the one that was already there — and never restored it. mode survived only because the dictionary had no key by that name.

4. The variables outlive the block

Related, and almost as surprising: the variables dict with creates are ordinary locals. They do not disappear at the closing brace.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
set c [dict create mode fast]
dict with c { }
puts "mode after the block: $mode"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
mode after the block: fast

dict with is not a scope. It is a command that sets some variables, runs your body, and reads them back.

5. What gets written back

Three rules, and they are not symmetric.

An existing key whose variable changed is written back:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
set d [dict create a 1]
dict with d { set a 42 }
puts $d
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
a 42

A brand-new variable does not become a key:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
set d [dict create a 1]
dict with d { set brandnew 99 }
puts "$d  | key added? [dict exists $d brandnew]"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
a 1  | key added? 0

Unsetting a variable does remove its key:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
set d [dict create a 1 b 2]
dict with d { unset b }
puts "$d  | b still there? [dict exists $d b]"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
a 1  | b still there? 0

So the block can change values and delete keys, and cannot add them. Section 1's missing total was this rule in action.

6. Both take a name

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
set c [dict create mode fast runs 3]
dict with $c { puts $mode }
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
can't read "mode fast runs 3": no such variable

$c substituted to the dictionary's text, and dict with then looked for a variable with that name.

Compare this with the same mistake on dict set, from Chapter 7.2 §7: there, Tcl happily created a variable with that name and the error was silent. Here it is trying to read one first, so you get told. The underlying mistake is identical; only the visibility differs — which is a good reason not to rely on being told.

7. The body failing does not prevent the write-back

Worth knowing before you put anything risky in the body:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
set g [dict create a 1]
catch {
    dict with g {
        set a 99
        error "something went wrong"
    }
}
puts $g
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
a 99

The body errored, and a is still 99. dict update behaves the same way:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
set h [dict create a 1]
catch {
    dict update h a x {
        set x 99
        error "something went wrong"
    }
}
puts $h
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
a 99

This is deliberate — it is what keeps the dictionary consistent with the variables when a body exits early, including via return or break. It is not a transaction. If you need all-or-nothing, work on a copy and assign it at the end:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
set safe [dict create a 1]
set attempt $safe
if {![catch {dict set attempt a 99 ; error "fail"}]} {
    set safe $attempt
}
puts $safe
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
a 1

8. dict update is the one to prefer

dict update avoids every problem above except the write-back-on-error rule:

  • It touches only the keys you name, so it cannot clobber a local you forgot about.
  • You choose the variable names, so you can avoid a collision deliberately.
  • It can create keys.
  • Reading the first line tells you exactly which keys the block is about.
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
set cfg [dict create mode fast runs 3 width 8]

dict update cfg runs cfg_runs width cfg_width {
    set cfg_runs  [expr {$cfg_runs * 2}]
    set cfg_width [expr {$cfg_width + 8}]
}
puts $cfg
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
mode fast runs 6 width 16

9. A worked example

A statistics accumulator, written three ways, all producing the same result:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
# 1 — plain dict get / dict set
proc record_plain {statsName ok} {
    upvar 1 $statsName s
    dict set s total [expr {[dict get $s total] + 1}]
    if {$ok} {
        dict set s pass [expr {[dict get $s pass] + 1}]
    }
}

# 2 — dict update, naming exactly what it touches
proc record_update {statsName ok} {
    upvar 1 $statsName s
    dict update s total t pass p {
        incr t
        if {$ok} { incr p }
    }
}

set stats [dict create total 0 pass 0]
foreach result {1 1 0 1 0} {
    record_update stats $result
}
puts $stats
puts "pass rate: [format %.1f [expr {100.0 * [dict get $stats pass] / [dict get $stats total]}]]%"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
total 5 pass 3
pass rate: 60.0%

Version 2 is shorter than version 1 and names its two keys on one visible line. A dict with version would be shorter still, and would also create locals called total and pass in a procedure that might well have had its own.

10. Exercises

Exercise 1. Predict all three lines:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
proc f {} {
    set width 100
    set d [dict create width 8]
    puts $width
    dict with d { puts $width }
    puts $width
}
f

Exercise 2. Why does this not add a total key, and what are two ways to make it?

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
set d [dict create a 2 b 3]
dict with d { set total [expr {$a * $b}] }

Exercise 3. What does dict with $d {...} report, and how does that differ from dict set $d k v?

Exercise 4. Rewrite this dict with as a dict update that cannot clobber anything:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
dict with cfg { set runs [expr {$runs * 2}] }

Exercise 5. After this runs, what is $d?

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
set d [dict create a 1 b 2]
catch { dict with d { set a 50 ; unset b ; error "stop" } }

Exercise 6. Write a procedure scale that multiplies every value in a dictionary by a factor and returns the new dictionary, without using dict with or dict update. Say why neither is a good fit here.

Answers

1.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
100
8
8

The dictionary's width key overwrote the local, and it was not restored.

2. Because dict with writes back only keys that already existed; a brand-new variable is discarded. Either seed the key first (dict create a 2 b 3 total 0), or use dict update d total t {set t ...}, which creates the key.

3. dict with $d gives can't read "...": no such variable — it tried to read a variable named after the dictionary's text. dict set $d k v creates such a variable instead and reports nothing at all, which is why it is the more dangerous of the two.

4.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
dict update cfg runs cfg_runs {
    set cfg_runs [expr {$cfg_runs * 2}]
}

It names the single key it touches, and cfg_runs is unlikely to collide with anything the surrounding procedure owns.

5. a 50. The body errored, and both the assignment and the deletion were still written back — b is gone and a is 50.

6.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
proc scale {d factor} {
    set out [dict create]
    dict for {k v} $d {
        dict set out $k [expr {$v * $factor}]
    }
    return $out
}
puts [scale [dict create a 1 b 2 c 3] 10]
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
a 10 b 20 c 30

Neither fits: both operate on keys known in advance, and this procedure must work on whatever keys it is given. dict for is the right tool for "every pair".

11. Remember

  • dict with name {body} makes a local variable for every key; dict update name key var ... {body} makes them only for the keys you name.
  • Both take the dictionary's bare name — they modify it. dict with $d errors; dict set $d does not.
  • dict with overwrites existing locals of the same name and does not restore them. This is its real cost.
  • The variables outlive the block. It is not a scope.
  • Changing a value writes back; unsetting a variable deletes the key; a new variable is discarded. dict update can create keys because you named them.
  • An error in the body does not prevent the write-back. Neither command is a transaction.
  • Prefer plain dict get/dict set; reach for dict update when a block is genuinely noisy. Use dict with only on dictionaries you built, in short blocks.
  • Read dict with fluently anyway — existing Tcl is full of it.

Where this fits

Part of the Tcl curriculum.