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 timetonumber() is safe: It returns nil on invalid input instead of throwing an error, making input validation straightforward
Comments
Loading comments...
Leave a Comment