Beginner

Variables and Types in Tcl

Learn how Tcl handles variables, its string-based type system, type conversions, and the everything-is-a-string philosophy in practice

Tcl’s approach to variables is unlike almost any other mainstream language. Rooted in the design philosophy that everything is a string, Tcl has no type declarations, no type keywords, and no compile-time type checks. A variable is simply a name bound to a string value, and the interpreter decides how to interpret that string based on the command using it.

This doesn’t mean Tcl is slow or imprecise. Modern Tcl interpreters keep an internal “dual representation” — a value can be cached as both a string and a more efficient form (integer, list, dict) at the same time, transparent to the programmer. From your code’s perspective, however, every value is conceptually a string.

In this tutorial you’ll see how to create variables with set, work with Tcl’s logical “types” (numbers, strings, lists, dictionaries, booleans), perform conversions, and use constants and namespaces. Because Tcl is a dynamic, string-based language, the focus is on what commands do with values rather than what type a variable was “declared” as.

Variable Assignment with set

Tcl has no = assignment operator. The set command both creates and updates variables, and $name retrieves a value.

Create a file named variables.tcl:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# Variables and Types in Tcl

# --- Basic assignment ---
set count 10
set price 19.95
set name "Ada"
set message {Tcl is "fun"}

puts "count   = $count"
puts "price   = $price"
puts "name    = $name"
puts "message = $message"

# --- set with no value reads the variable ---
puts "set returns: [set count]"

# --- Reassignment: same command, no special syntax ---
set count 42
puts "count   = $count (after reassignment)"

# --- unset removes a variable ---
set temp "delete me"
unset temp
puts "temp exists? [info exists temp]"

Notice that strings, integers, and floats all use the same set syntax. There is no syntactic difference between assigning a number and assigning a string — both are just strings as far as Tcl is concerned. The info exists command tests whether a variable is currently bound.

Logical Types and Their Commands

Although every value is a string, Tcl commands interpret values as numbers, booleans, lists, or dictionaries depending on context. The classifications below are conventions enforced by commands, not type declarations.

Create a file named types.tcl:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# Logical "types" in Tcl

# --- Numbers: integers and floating point ---
set i 42
set big 0xFF
set neg -7
set pi 3.14159
set sci 6.022e23

puts "Integer (decimal): $i"
puts "Integer (hex):     $big -> [expr {$big}]"
puts "Negative:          $neg"
puts "Float:             $pi"
puts "Scientific:        $sci"

# expr is the gateway to numeric evaluation
puts "i is integer? [string is integer -strict $i]"
puts "pi is integer? [string is integer -strict $pi]"
puts "pi is double?  [string is double  -strict $pi]"

# --- Strings ---
set greeting "Hello"
set quoted   {This {keeps} braces literal}
puts "Length of greeting: [string length $greeting]"
puts "Upper:              [string toupper $greeting]"
puts "Quoted literal:     $quoted"

# --- Booleans: any of these are valid true/false ---
set yes  true
set no   false
set one  1
set zero 0

puts "yes  -> [expr {$yes  ? {truthy} : {falsy}}]"
puts "zero -> [expr {$zero ? {truthy} : {falsy}}]"

# --- Lists: space-separated strings, the Tcl way ---
set fruits {apple banana cherry}
puts "Fruits:        $fruits"
puts "Length:        [llength $fruits]"
puts "First:         [lindex $fruits 0]"
puts "After append:  [lappend fruits date]"

# --- Dictionaries: key/value pairs since Tcl 8.5 ---
set person [dict create name Grace age 85 field "computer science"]
puts "Name:    [dict get $person name]"
puts "Age:     [dict get $person age]"
dict set person age 86
puts "Updated: [dict get $person age]"

Key observation: string is integer and string is double let you classify a value at runtime. Lists are simply strings whose words are separated by whitespace — {apple banana cherry} is both a string and a three-element list, depending on which command consumes it.

Conversions and the expr Command

Because everything is a string, “type conversion” usually means asking a command to interpret a string in a particular way. The expr command is Tcl’s mathematical evaluator, and format / scan are the printf/scanf-style converters.

Create a file named conversions.tcl:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# Type conversion in Tcl

# --- String to number: just use it in expr ---
set s "123"
set n [expr {$s + 0}]
puts "string \"$s\" -> integer $n  ([string is integer -strict $n])"

set f [expr {"3.14" * 1.0}]
puts "string \"3.14\" -> double  $f  ([string is double -strict $f])"

# --- Number to string: format it ---
set value 255
puts "Decimal: [format %d   $value]"
puts "Hex:     [format %#x  $value]"
puts "Octal:   [format %#o  $value]"
puts "Binary:  [format %b   $value]"
puts "Padded:  [format %05d $value]"
puts "Float:   [format %.3f [expr {$value / 7.0}]]"

# --- scan parses strings into values ---
scan "age=42" "age=%d" age
puts "Parsed age: $age"

# --- Integer / float coercion happens automatically in expr ---
set mixed [expr {3 + 2.5}]
puts "3 + 2.5 = $mixed (now a double)"

# --- Lists and strings are interchangeable ---
set csv "red,green,blue"
set words [split $csv ","]
puts "Split list: $words  (length [llength $words])"
puts "Rejoined:   [join $words { | }]"

The expression "3.14" * 1.0 shows the philosophy clearly: a quoted string can participate in arithmetic, because expr reads its argument as a math expression and re-parses the string as a number. If the string isn’t a valid number, you get an error at runtime — there’s no compile-time check.

Constants, Scope, and Namespaces

Tcl has no const keyword, but you can simulate immutability with a procedure or with namespace conventions. Variables in Tcl are local to a procedure by default; outside of any procedure, they live in the global namespace.

Create a file named scope.tcl:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# Variable scope and pseudo-constants in Tcl

# --- Global variables ---
set greeting "Hello"

proc show_greeting {} {
    # Without 'global', $greeting would be undefined inside the proc
    global greeting
    puts "Inside proc: $greeting"
}
show_greeting

# --- Local variables shadow globals ---
proc local_demo {} {
    set greeting "Local hello"
    puts "Local:  $greeting"
}
local_demo
puts "Global: $greeting"

# --- Pseudo-constants via namespace ---
namespace eval Const {
    variable PI    3.14159265358979
    variable E     2.71828182845904
    variable MAX_USERS 100
}

proc circle_area {radius} {
    variable Const::PI
    return [expr {$Const::PI * $radius * $radius}]
}

puts "Area of r=5: [circle_area 5]"
puts "PI    = $Const::PI"
puts "MAX   = $Const::MAX_USERS"

# --- The 'unset -nocomplain' idiom for safe deletion ---
unset -nocomplain does_not_exist
puts "Safely tried to unset a missing variable."

Tcl’s scoping is lexical inside procedures, dynamic-feeling outside. A procedure cannot see global variables unless you explicitly say global name or reference them with their fully-qualified name (e.g., ::greeting). Namespaces let you group related variables and procedures, and act as a lightweight stand-in for modules.

Running with Docker

1
2
3
4
5
6
7
8
# Pull the official image
docker pull efrecon/tcl:latest

# Run each example
docker run --rm -v $(pwd):/app -w /app efrecon/tcl:latest /app/variables.tcl
docker run --rm -v $(pwd):/app -w /app efrecon/tcl:latest /app/types.tcl
docker run --rm -v $(pwd):/app -w /app efrecon/tcl:latest /app/conversions.tcl
docker run --rm -v $(pwd):/app -w /app efrecon/tcl:latest /app/scope.tcl

Expected Output

Output from variables.tcl:

count   = 10
price   = 19.95
name    = Ada
message = Tcl is "fun"
set returns: 10
count   = 42 (after reassignment)
temp exists? 0

Output from types.tcl:

Integer (decimal): 42
Integer (hex):     0xFF -> 255
Negative:          -7
Float:             3.14159
Scientific:        6.022e23
i is integer? 1
pi is integer? 0
pi is double?  1
Length of greeting: 5
Upper:              HELLO
Quoted literal:     This {keeps} braces literal
yes  -> truthy
zero -> falsy
Fruits:        apple banana cherry
Length:        3
First:         apple
After append:  apple banana cherry date
Name:    Grace
Age:     85
Updated: 86

Output from conversions.tcl:

string "123" -> integer 123  (1)
string "3.14" -> double  3.14  (1)
Decimal: 255
Hex:     0xff
Octal:   0377
Binary:  11111111
Padded:  00255
Float:   36.429
Parsed age: 42
3 + 2.5 = 5.5 (now a double)
Split list: red green blue  (length 3)
Rejoined:   red | green | blue

Output from scope.tcl:

Inside proc: Hello
Local:  Local hello
Global: Hello
Area of r=5: 78.53981633974475
PI    = 3.14159265358979
MAX   = 100
Safely tried to unset a missing variable.

Key Concepts

  • set name value is the only assignment — there is no = operator, and the same command both creates and updates variables.
  • Everything is a string — numbers, booleans, lists, and dictionaries are all stored as strings; commands interpret them according to their needs.
  • No type declarations — Tcl is dynamic and string-based. Validate at runtime with string is integer, string is double, string is boolean, etc.
  • expr is the math gateway — arithmetic and comparisons require expr, which parses its argument as a numeric expression. Always brace expressions: expr {$a + $b}.
  • Lists are just whitespace-separated strings — use llength, lindex, lappend, split, and join to manipulate them; no separate “array literal” syntax exists.
  • Dictionaries (since Tcl 8.5) provide key/value storage via the dict command family while still being interchangeable with strings.
  • Scope is local by default in procedures — use global or fully qualified names like ::var or ns::var to reach outside variables.
  • Use namespace eval for pseudo-constants — Tcl has no const, but namespaces let you group related immutable values and reference them with ::ns::name.

Running Today

All examples can be run using Docker:

docker pull efrecon/tcl:latest
Last updated:

Comments

Loading comments...

Leave a Comment

2000 characters remaining