Skip to content
VLSI Mentor

Tcl Part I · Module 11 · Scope and Namespaces

Tcl's Scope Rules: Global and Local

Chapter 10.6 established that a procedure cannot see its caller's variables. This module is about what you can do when you need to, and every one of those tools is easier to use correctly if you first have an accurate picture of what it is bending. That picture is smaller than most people assume. Tcl does not have a chain of enclosing scopes that a name is searched through; it has exactly two kinds of scope, and a name is found in one of them or nowhere. Getting that right now means the commands in the rest of the module read as deliberate exceptions rather than as a system you are still guessing at.

Foundation11 min readTclPart IScopeVariablesProcedures

Part I · Module 11 · Chapter 11.1 · Tcl's Scope Rules: Global and Local

1. Two kinds of scope

  • One global scope. It exists for the life of the interpreter. Variables set at the top level of a script live here.
  • One local scope per procedure call. It is created when the call starts and discarded when it returns.

That is the whole system. There is no third kind, and — the part that matters — they do not nest.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
set x 1

proc f {} {
    set x 2
    puts "inside f, x is $x"
}

f
puts "outside, x is $x"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
inside f, x is 2
outside, x is 1

Two variables that happen to share a name. The procedure did not change the global one and was never able to see it.

Two parallel left-to-right flows. The global scope exists once and always; a variable set there holds one and is still one after a procedure returns. A local scope is created per procedure call; a variable of the same name set inside the procedure holds two and is discarded when the procedure returns.Global scopeone, always presentx is 1set at top levelStill 1after the call returnsLocal scopeone per callx is 2set inside the procDiscardedwhen the call returns12
Figure 1 — the two scopes and what happens to each. The global scope exists once and lasts as long as the interpreter, so a variable set there keeps its value across any number of procedure calls. A local scope is created fresh for each call and thrown away when that call returns, so a variable of the same name set inside a procedure is a different variable with a different lifetime.

2. Scopes do not nest

This is where the model usually goes wrong, because many languages do nest. In Python or JavaScript, a function defined inside another can read the outer one's variables. Tcl does not work that way, and a procedure called from another procedure can see neither the caller's locals nor the globals:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
set x 1

proc outer {} {
    set y "outer-local"
    return [inner]
}

proc inner {} {
    set seen {}
    lappend seen [expr {[info exists y] ? "sees y" : "no y"}]
    lappend seen [expr {[info exists x] ? "sees x" : "no x"}]
    return $seen
}

puts [outer]
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
{no y} {no x}

Both invisible. inner's frame contains its own parameters and whatever it sets, and nothing else. There is no search outward.

3. Commands are the exception, and they always were

A procedure defined inside another procedure is not local to it:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
proc definer {} {
    proc made_inside {} {
        return "I exist globally"
    }
    return "defined"
}

puts [definer]
puts [made_inside]
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
defined
I exist globally

made_inside was created while definer was running and is callable from anywhere afterwards. That is Chapter 10.1 §2's flat command table: proc registers into one global table regardless of where it runs.

So the rule has two halves, and they point in opposite directions:

Where it livesWho can reach it
Commandsone flat global tableeverything, always
Variablesthe frame that set themthat frame only

Every tool in this module exists because the second row is sometimes too restrictive.

4. The global scope is just the top level

A small confirmation that the model is right rather than approximately right. global — which Chapter 11.2 covers properly — does nothing at the top level:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
global zzz
puts "zzz exists? [info exists zzz]"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
zzz exists? 0

No error, and no variable created. global links a local name to the global scope, and at the top level the local scope is the global scope, so there is nothing to link. The command is a no-op rather than a special case.

5. Each call gets its own frame

Not each procedure — each call. Recursion makes that visible:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
proc countdown {n} {
    if {$n <= 0} { return "done" }
    set marker "frame for n=$n"
    set result [countdown [expr {$n - 1}]]
    return "$marker -> $result"
}

puts [countdown 3]
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
frame for n=3 -> frame for n=2 -> frame for n=1 -> done

Three live frames at the deepest point, each with its own marker and its own n. They do not interfere, because each call allocated its own.

6. What this means in practice

Three consequences worth naming now, because the rest of the module is about working with them.

A procedure's behaviour is determined by its arguments — there is no hidden context it might be reading, unless it explicitly asks for one. That is what makes Chapter 10.10's "checkable on its own" criterion achievable at all.

You cannot accidentally clobber a caller's variable. Pick any local name you like; it cannot collide with anything outside.

Sharing has to be deliberate. Every mechanism in this module — global, upvar, uplevel, namespace variables — is an explicit opt-in. None of them happens by default, which is why a Tcl procedure that looks self-contained usually is.

7. A worked example

A counter written three ways, showing what the model forces — including a failure that does not announce itself:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
# 1. Does not work, and does not say so
set count 0
proc bump_broken {} {
    incr count
}
bump_broken
bump_broken
bump_broken
puts "broken : the global count is still $count"

# 2. Works, by passing in and returning out
proc bump_value {n} {
    return [expr {$n + 1}]
}
set value 0
set value [bump_value $value]
set value [bump_value $value]
puts "value  : $value"

# 3. Works, by asking for the global explicitly
set total 0
proc bump_global {} {
    global total
    incr total
}
bump_global
bump_global
puts "global : $total"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
broken : the global count is still 0
value  : 2
global : 2

The first version is the one worth studying. It raised no error, because incr creates a missing variable at zero (Chapter 3.9) — so each call made a brand-new local count, set it to 1, and threw it away on return. Three calls, no effect, no complaint.

The second version is the shape this module will keep recommending; the third is covered next, together with the reasons to prefer the second.

8. Exercises

Exercise 1. How many kinds of scope does Tcl have, and when is each created and destroyed?

Exercise 2. What does this print?

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
set v 1
proc f {} { set v 2 }
f
puts $v

Exercise 3. Can a procedure called from another procedure see the caller's local variables? What about the globals?

Exercise 4. A procedure defines another procedure inside its body. After it returns, is the inner one callable? Why?

Exercise 5. Explain why global zzz at the top level does nothing.

Exercise 6. Someone says Tcl variables "shadow" outer ones inside a procedure. What is wrong with that word here?

Answers

1. Two. One global scope, created when the interpreter starts and lasting until it exits; and one local scope per procedure call, created when the call begins and discarded when it returns.

2. 1. The procedure set its own local v; the global was never touched.

3. Neither. A procedure's frame holds only its parameters and what it sets. The caller's locals are invisible, and so are the globals until the procedure asks for one by name with global or ::.

4. Yes, from anywhere. proc registers into one flat, global command table regardless of where it runs, so the inner definition is not local to the outer procedure in the way a local variable would be.

5. Because global links a local name to the global scope, and at the top level the local scope is the global scope. There is nothing to link, so the command does nothing — and creates no variable.

6. "Shadowing" implies the outer variable is still there, hidden behind the inner one, and could be reached by removing the inner. In Tcl the outer variable is not present in the procedure's frame at all — it is not behind anything. The accurate word is that it is simply not in scope.

9. Remember

  • Two kinds of scope: one global, and one local per procedure call. Nothing else.
  • They do not nest. A procedure called from another sees neither its caller's locals nor the globals.
  • Each call gets its own frame, which is why recursion works without interference.
  • Commands are the exception — one flat global table, so a procedure defined anywhere is callable everywhere.
  • global at the top level is a no-op, because the local scope there already is the global scope.
  • A procedure's behaviour depends only on its arguments unless it explicitly asks for more.
  • You cannot accidentally clobber a caller's variable, so local names are free to choose.
  • Sharing is always an explicit opt-in — global, upvar, uplevel and namespace variables are the ways, and the rest of this module covers each.

Where this fits

Part of the Tcl curriculum.