Skip to content
VLSI Mentor

Tcl Part I · Module 7 · Dictionaries and Arrays

dict exists: Asking Before Reading

The last chapter ended on an error: asking a dictionary for a key it does not have stops the script. Often that is exactly right. Often it is not, and what you actually wanted was a sensible default. This chapter gives you the test that distinguishes those cases, the idiom for each, and a clear argument for why reaching automatically for a default is the wrong habit. It also shows where the obvious check is not as safe as it looks, because dict exists protects you from a missing key and not from a malformed one.

Foundation11 min readTclPart IDictionariesdict existsDefaults

Part I · Module 7 · Chapter 7.3 · dict exists: Asking Before Reading

1. The command

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

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

It returns 1 or 0, and it takes a value — $cfg, not cfg — because it only reads. That is the rule from Chapter 7.2 §7.

2. Filling in a default

The most common use is to supply a value for a key the caller did not set:

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

if {![dict exists $cfg runs]} {
    dict set cfg runs 1
}

puts $cfg
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
mode fast runs 1

Note that dict set here takes cfg — bare — because it is modifying. The dict exists on the line above takes $cfg. Both appear in three lines of code, which is a good reason to be deliberate about it.

If you only want the value and do not want to change the dictionary, use the conditional operator from Chapter 4.7:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
set cfg [dict create mode fast]
set runs [expr {[dict exists $cfg runs] ? [dict get $cfg runs] : 1}]
puts "runs = $runs"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
runs = 1

3. A small helper is usually better

Written more than twice, that pattern wants a name:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
proc dget {d key {default ""}} {
    if {[dict exists $d $key]} {
        return [dict get $d $key]
    }
    return $default
}

set cfg [dict create mode fast]
puts [dget $cfg mode  slow]
puts [dget $cfg runs  1]
puts "[dget $cfg nothing]<-"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
fast
1
<-

Four lines, and every read site gets shorter and says what it means.

4. The alternative: catch

Chapter 1.9 introduced errors; catch lets you run something that might fail and inspect the result instead of stopping:

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

if {[catch {dict get $cfg runs} value]} {
    set value 1
}
puts "runs = $value"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
runs = 1

This works. Prefer dict exists anyway. catch traps any failure, so a genuine mistake — a misspelled subcommand, a malformed dictionary — comes back looking exactly like an absent key, and you silently substitute a default for it. dict exists asks the narrow question you actually meant.

5. Empty is not missing

A trap worth seeing once:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
set cfg [dict create blank "" mode fast]

puts "exists blank : [dict exists $cfg blank]"
puts "get blank    : '[dict get $cfg blank]'"
puts "exists gone  : [dict exists $cfg gone]"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
exists blank : 1
get blank    : ''
exists gone  : 0

The key blank exists and its value is the empty string. So this test is wrong:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
# WRONG — cannot distinguish "absent" from "present but empty"
if {[dget $cfg blank] eq ""} {
    puts "treating blank as unset"
}
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
treating blank as unset

If the difference matters — and in configuration handling it usually does, because "the user explicitly set this to nothing" is not the same as "the user said nothing" — test with dict exists and never by comparing the value.

6. Where dict exists is not as safe as it looks

dict exists never complains about a key that is simply absent. It will still raise an error if the dictionary, or something on the path through it, is not a valid dictionary.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
set d [dict create a 1 b 2]
puts [dict exists $d zz]
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
0

Safe. Now ask for a two-level path:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
puts [dict exists $d a b]
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
missing value to go with key

That is an error, not a 0. Asking for a b tells Tcl to look up a, then treat its value as a dictionary and look up b inside it. The value of a is 1 — a single word, an odd number, and therefore not a dictionary at all. The same happens if the outer value is malformed:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
puts [dict exists "a 1 b" a]
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
missing value to go with key

Deep paths are safe when the structure is actually nested:

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

The second one returns 0 rather than erroring, because stages really is a dictionary — nope is just not a key in it. Chapter 7.6 covers this properly.

7. When a default is the wrong answer

Everything so far shows how to avoid the error. The harder question is when you should not.

A default is right when the key is genuinely optional and there is an obvious, safe value to assume:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
set verbose [dget $opts verbose 0]
set retries [dget $opts retries 1]

Nobody is harmed by assuming quiet output and one attempt.

A default is wrong when the key's absence means something has gone wrong upstream:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
# BAD — silently analyses nothing when the field is missing
set target [dget $job target ""]
run_on $target

If target is missing, the job description is broken. Substituting an empty string turns a clear failure into a run that does nothing, or does something to the wrong thing, and reports success. Here you want the error:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
# GOOD — a missing target stops the script, loudly
set target [dict get $job target]

Or, if you want a message a human can act on:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
if {![dict exists $job target]} {
    error "job description has no 'target' field"
}
set target [dict get $job target]
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
job description has no 'target' field

8. A worked example

A settings merger that fills in defaults, validates what must be present, and reports clearly:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
proc dget {d key {default ""}} {
    if {[dict exists $d $key]} { return [dict get $d $key] }
    return $default
}

proc normalise {job} {
    foreach required {name target} {
        if {![dict exists $job $required]} {
            error "job is missing required field '$required'"
        }
    }
    dict set job retries [dget $job retries 1]
    dict set job verbose [dget $job verbose 0]
    return $job
}

set good [dict create name smoke target top]
puts [normalise $good]

set bad [dict create name smoke]
if {[catch {normalise $bad} msg]} {
    puts "rejected: $msg"
}
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
name smoke target top retries 1 verbose 0
rejected: job is missing required field 'target'

Two kinds of missing key, handled two different ways on purpose. That is the chapter.

9. Exercises

Exercise 1. What do these print?

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

Exercise 2. Rewrite this using dict exists and say what it gains:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
if {[catch {set w [dict get $p width]}]} { set w 1 }

Exercise 3. Explain why dict exists $d a b can error even though dict exists $d zz cannot, given set d [dict create a 1].

Exercise 4. For each, say default or error and why: (a) a missing color for a report; (b) a missing output_file for a script that writes one; (c) a missing timeout where 60 is the documented default; (d) a missing password for a login.

Exercise 5. Write require — a procedure taking a dictionary and a list of key names that raises a single error naming every missing key at once, rather than stopping at the first.

Exercise 6. Why is [dget $cfg x] eq "" not a correct test for "the key x is absent"? Give a dictionary that proves it.

Answers

1. 1 then 0. The key b exists; its value just happens to be empty.

2.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
if {[dict exists $p width]} { set w [dict get $p width] } else { set w 1 }

It gains precision: the original would also silently default if p were malformed or the subcommand misspelled, hiding a real bug behind a plausible value.

3. zz is a one-key lookup — absent simply means 0. a b is a path: Tcl must read a's value, 1, and treat it as a dictionary in order to look for b. A single word is an odd-length dictionary, so that step errors before any question about b is asked.

4. (a) default — a report without a colour preference is fine. (b) error — writing somewhere unspecified is worse than not running. (c) default — it is documented, so the absence is expected. (d) error — never invent a credential.

5.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
proc require {d keys} {
    set missing {}
    foreach k $keys {
        if {![dict exists $d $k]} { lappend missing $k }
    }
    if {[llength $missing]} {
        error "missing required field(s): [join $missing {, }]"
    }
}

if {[catch {require [dict create name smoke] {name target mode}} m]} { puts $m }
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
missing required field(s): target, mode

Naming all of them at once saves the reader three runs.

6. Because a key that exists with an empty value gives the same answer as one that is absent:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
set cfg [dict create blank ""]
puts "exists: [dict exists $cfg blank]"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
exists: 1

The test says "absent"; dict exists says it is present. Use dict exists.

10. Remember

  • dict exists $d key returns 1 or 0 and takes a value, not a variable name.
  • Use it to fill a default, either with an if that modifies, or with ? : when you only want the value.
  • Wrap the pattern in a small dget helper — Tcl 8.7 adds dict getwithdefault, but it does not exist on 8.6 or earlier.
  • Prefer dict exists to catch — catch hides real failures behind the same path as an absent key.
  • A key with an empty value is present. Never test absence by comparing the value to "".
  • dict exists is a key test, not a type test — a path that descends through a non-dictionary value still errors with missing value to go with key.
  • Decide deliberately between a default and a failure. Optional with a safe assumption: default. Absence means something is broken: let it stop.

Where this fits

Part of the Tcl curriculum.