Beginner

Variables and Types in Lua

Learn about variables, data types, and type conversions in Lua with practical Docker-ready examples

Lua takes a minimalist approach to its type system. With only eight basic types and no type declarations required, Lua lets you focus on solving problems rather than satisfying a compiler. Variables spring into existence the moment you assign a value to them, and they can hold any type at any time.

What makes Lua’s type system interesting is its combination of dynamic typing with strong typing. Variables themselves have no type—values do. You can reassign a variable from a number to a string without complaint, but Lua won’t silently coerce a string into a number during arithmetic (unless the string actually represents a valid number). This gives you flexibility without the hidden bugs that come from truly weak typing.

In this tutorial, you’ll learn about Lua’s eight basic types, how to declare local and global variables, how type coercion works, and how Lua 5.4’s const variables add a touch of immutability.

Lua’s Eight Basic Types

Lua has exactly eight types: nil, boolean, number, string, table, function, userdata, and thread. You can check any value’s type with the built-in type() function, which always returns a string.

Create a file named variables.lua:

 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
-- Lua's Eight Basic Types

-- nil: the absence of a value
local nothing = nil
print("nil:", nothing, type(nothing))

-- boolean: true or false (only false and nil are falsy)
local active = true
local deleted = false
print("boolean:", active, type(active))

-- number: integers and floats (unified in Lua 5.3+)
local count = 42           -- integer
local pi = 3.14159         -- float
local big = 1e10           -- scientific notation (float)
local hex = 0xFF           -- hexadecimal (integer 255)
print("integer:", count, type(count))
print("float:", pi, type(pi))

-- string: immutable sequences of bytes
local greeting = "Hello, Lua"
local multiline = [[
This is a
multiline string]]
local length = #greeting   -- # operator gives string length
print("string:", greeting, type(greeting))
print("string length:", length)

-- function: first-class values
local square = function(x) return x * x end
print("function:", square, type(square))
print("square(7):", square(7))

-- table: the universal data structure
local colors = {"red", "green", "blue"}
local point = {x = 10, y = 20}
print("table:", colors, type(colors))
print("point.x:", point.x)

-- type() always returns a string
print()
print("type() returns:", type(type(42)))

Local vs Global Variables

One of the most important concepts in Lua is the distinction between local and global variables. Variables are global by default, which is a common source of bugs. Always use the local keyword to limit a variable’s scope.

Create a file named variables_scope.lua:

 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
-- Local vs Global Variables

-- Global variable (avoid in real code)
message = "I'm global"

-- Local variable (preferred)
local secret = "I'm local"

print("global:", message)
print("local:", secret)

-- Scope demonstration with blocks
do
    local inner = "only visible here"
    print("inside block:", inner)
end
-- print(inner)  -- Would print nil: inner is out of scope

-- Local variables shadow outer ones
local value = "outer"
do
    local value = "inner"
    print("shadowed:", value)
end
print("original:", value)

-- Multiple assignment
local a, b, c = 1, "two", true
print("multiple:", a, b, c)

-- Extra values are discarded, missing values become nil
local x, y = 10, 20, 30   -- 30 is discarded
local p, q = "only one"    -- q is nil
print("extra discarded:", x, y)
print("missing is nil:", p, q)

-- Swapping values (no temp variable needed)
local first, second = "alpha", "beta"
first, second = second, first
print("swapped:", first, second)

-- Lua 5.4: const variables (cannot be reassigned)
local MAX <const> = 100
local PI <const> = 3.14159
print("const MAX:", MAX)
print("const PI:", PI)
-- MAX = 200  -- Would cause a compile error!

Type Coercion and Conversion

Lua performs automatic coercion between strings and numbers in some contexts, but it’s better to be explicit. The tonumber() and tostring() functions handle conversions safely.

Create a file named variables_conversion.lua:

 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
50
51
52
53
54
55
56
57
-- Type Coercion and Conversion

-- Automatic coercion: strings to numbers in arithmetic
local result = "10" + 5
print("'10' + 5 =", result, type(result))

-- But concatenation (..) coerces numbers to strings
local text = "Value: " .. 42
print(text, type(text))

-- Explicit conversion with tonumber()
local input = "3.14"
local num = tonumber(input)
print("tonumber('3.14'):", num, type(num))

-- tonumber() returns nil for invalid input (safe!)
local bad = tonumber("hello")
print("tonumber('hello'):", bad)

-- tonumber() with base for other number systems
local binary = tonumber("1010", 2)    -- binary to decimal
local octal = tonumber("77", 8)       -- octal to decimal
local hexval = tonumber("FF", 16)     -- hex to decimal
print("binary 1010:", binary)
print("octal 77:", octal)
print("hex FF:", hexval)

-- Explicit conversion with tostring()
local n = 42
local s = tostring(n)
print("tostring(42):", s, type(s))

-- Integer and float distinction (Lua 5.3+)
local int_val = 42
local float_val = 42.0
print("integer:", int_val, "is integer?", math.type(int_val))
print("float:", float_val, "is integer?", math.type(float_val))

-- Converting between integer and float
local as_float = int_val + 0.0
local as_int = math.floor(float_val)
print("to float:", as_float, math.type(as_float))
print("to int:", as_int, math.type(as_int))

-- Truthiness: only false and nil are falsy
-- 0, "", and empty tables are all truthy!
local values = {false, nil, 0, "", {}, true, 42}
local labels = {"false", "nil", "0", '""', "{}", "true", "42"}
print()
print("Truthiness in Lua:")
for i = 1, #labels do
    if values[i] then
        print("  " .. labels[i] .. " is truthy")
    else
        print("  " .. labels[i] .. " is falsy")
    end
end

Running with Docker

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# Pull the Lua image
docker pull nickblah/lua:5.4-alpine

# Run the basic types example
docker run --rm -v $(pwd):/app -w /app nickblah/lua:5.4-alpine lua variables.lua

# Run the scope example
docker run --rm -v $(pwd):/app -w /app nickblah/lua:5.4-alpine lua variables_scope.lua

# Run the conversion example
docker run --rm -v $(pwd):/app -w /app nickblah/lua:5.4-alpine lua variables_conversion.lua

Expected Output

Output from variables.lua:

nil:	nil	nil
boolean:	true	boolean
integer:	42	number
float:	3.14159	number
string:	Hello, Lua	string
string length:	10
function:	function: 0x...	function
square(7):	49
table:	table: 0x...	table
point.x:	10

type() returns:	string

Output from variables_scope.lua:

global:	I'm global
local:	I'm local
inside block:	only visible here
shadowed:	inner
original:	outer
multiple:	1	two	true
extra discarded:	10	20
missing is nil:	only one	nil
swapped:	beta	alpha
const MAX:	100
const PI:	3.14159

Output from variables_conversion.lua:

'10' + 5 =	15	number
Value: 42	string
tonumber('3.14'):	3.14	number
tonumber('hello'):	nil
binary 1010:	10
octal 77:	63
hex FF:	255
tostring(42):	42	string
integer:	42	is integer?	integer
float:	42.0	is integer?	float
to float:	42.0	float
to int:	42	integer

Truthiness in Lua:
  false is falsy
  nil is falsy
  0 is truthy
  "" is truthy
  {} is truthy
  true is truthy
  42 is truthy

Key Concepts

  • Eight types total: nil, boolean, number, string, table, function, userdata, and thread — that’s the entire type system
  • Dynamic but strong: Variables have no type, but values do — Lua won’t silently convert between incompatible types in most operations
  • Local by default is a lie: Variables are global by default; always use local to avoid polluting the global scope
  • Multiple assignment: Lua supports assigning multiple variables in one statement, with extras discarded and missing values set to nil
  • Truthiness is simple: Only false and nil are falsy — 0, empty strings, and empty tables are all truthy (unlike Python or JavaScript)
  • Integer/float split: Since Lua 5.3, numbers are either integers or floats internally, checked with math.type()
  • const in Lua 5.4: The <const> attribute creates variables that cannot be reassigned, catching accidental mutations at compile time
  • tonumber() is safe: It returns nil on invalid input instead of throwing an error, making input validation straightforward

Running Today

All examples can be run using Docker:

docker pull nickblah/lua:5.4-alpine
Last updated:

Comments

Loading comments...

Leave a Comment

2000 characters remaining