Basics
Lua has eight types and no type declarations. Most of the surprises for people arriving from other languages come from three places: globals are the default, nil is a real value with real consequences, and numbers quietly split into integers and floats in Lua 5.3+.
The eight types
Section titled “The eight types”type(v) returns one of exactly eight strings. There are no others.
print(type(nil)) -- => nilprint(type(true)) -- => booleanprint(type(42)) -- => numberprint(type("hi")) -- => stringprint(type(print)) -- => functionprint(type({})) -- => tableprint(type(io.stdout)) -- => userdataprint(type(coroutine.create(function() end))) -- => thread| Type | What it is |
|---|---|
nil |
The absence of a value. Only one value: nil. |
boolean |
true and false. |
number |
Integers and floats (see below). |
string |
Immutable, 8-bit clean byte strings. |
function |
Lua functions and C functions, indistinguishable from Lua. |
table |
Associative arrays. The only composite type. |
userdata |
A block of raw C memory owned by the host. You can’t create one in pure Lua. |
thread |
A coroutine. Not an OS thread. |
Values are typed; variables are not. A variable is just a name bound to a value, and it can be rebound to a value of any type.
local x = 10x = "now a string"x = function() return 1 endx = { 1, 2, 3 } -- all legalnil, boolean and number are value types — assignment copies them. string, function, table, userdata and thread are reference types; assignment copies the reference. Strings are immutable so the distinction is invisible for them.
local a = { 1, 2 }local b = ab[1] = 99print(a[1]) -- => 99 (same table)userdata and thread, briefly
Section titled “userdata and thread, briefly”userdata is how a host program hands you a C object: a file handle from io.open, a socket, a game entity. It is opaque — you interact with it only through methods the host installs via a metatable.
local f = io.open("/etc/hostname", "r")print(type(f)) -- => userdataf:close()thread is a coroutine, covered on the standard library page. Lua has no OS threads and no shared-memory concurrency in the core language.
Comments
Section titled “Comments”-- a line comment
--[[ a block comment spanning lines ]]
--[==[ a block comment that can contain ]] safely ]==]The [==[ ... ]==] form uses matching numbers of = signs, which is how you nest or embed brackets. A useful trick: adding one - toggles a block of code on and off.
--[[print("disabled")--]]
---[[print("enabled") -- the leading --- makes the opener a line comment--]]Variables: local vs global
Section titled “Variables: local vs global”An unqualified assignment creates a global. This is the single most consequential default in the language.
count = 0 -- globallocal count = 0 -- local to the enclosing block/chunkGlobals are not a separate namespace mechanism — they are entries in a table. Every function has an upvalue named _ENV, and the compiler rewrites count into _ENV.count. At the top level _ENV is the global table _G.
x = 1print(_G.x) -- => 1print(_ENV.x) -- => 1_G.y = 2print(y) -- => 2That means every global read is a hash table lookup, while a local is a slot in the function’s register frame. Locals are meaningfully faster in hot code, and much friendlier to LuaJIT.
The standard mitigation is to cache library functions in locals at the top of a module, which is both faster and self-documenting:
local fmt = string.formatlocal insert = table.insertlocal floor = math.floorScope rules
Section titled “Scope rules”A local is visible from after its declaration statement to the end of the enclosing block. Blocks are created by do ... end, function bodies, loop bodies, and if branches.
local v = 1do local v = 2 print(v) -- => 2endprint(v) -- => 1Because visibility starts after the declaration, this does not do what it looks like:
local x = 10local x = x + 1 -- the x on the right is the OLD xprint(x) -- => 11And recursion needs local function, which declares the name before compiling the body:
local function fact(n) if n <= 1 then return 1 end return n * fact(n - 1) -- worksend
-- the desugared form does NOT work:local fact2 = function(n) return n * fact2(n - 1) end -- fact2 is nil insideVariable attributes (Lua 5.4)
Section titled “Variable attributes (Lua 5.4)”Lua 5.4 adds two attributes in angle brackets:
local MAX <const> = 100-- MAX = 200 --> compile error: attempt to assign to const variable 'MAX'
local f <close> = io.open("data.txt")-- f's __close metamethod runs when the block exits, even on error<const> is checked at compile time and lets the compiler inline the value. <close> is for deterministic cleanup: the value’s __close metamethod is called when the variable goes out of scope. Neither exists in 5.1/5.3.
nil is the value of anything that has not been assigned. Reading an undeclared global or a missing table key is not an error — you get nil.
print(undefined_thing) -- => nillocal t = {}print(t.missing) -- => nilAssigning nil deletes a table key. There is no distinction between “key absent” and “key present with value nil”:
local t = { a = 1 }t.a = nilfor k in pairs(t) do print(k) end -- prints nothingWhere nil bites:
- Calling or indexing it is a runtime error:
attempt to index a nil value/attempt to call a nil value. This is the most common Lua error, and it usually means a typo in a name or a failedrequire. - A
nilin the middle of an array breaks#andipairs(see Tables). nilcan’t be a table key at all:t[nil] = 1raisestable index is nil.
Booleans and truthiness
Section titled “Booleans and truthiness”Only two values are false: nil and false. Everything else is true — including 0, "", {} and NaN.
if 0 then print("zero is true") end -- printsif "" then print("empty is true") end -- printsif not nil then print("nil is false") end -- printsThis is a genuine simplification compared to JavaScript or Python, but it means you must test explicitly for emptiness:
if #s == 0 then ... end -- empty stringif next(t) == nil then ... end -- empty tableComparison operators always return a real boolean. == compares references for tables/functions and values for numbers/strings; it never coerces between types:
print(1 == "1") -- => false (no coercion in comparison)print("a" < "b") -- => true (byte order, locale-dependent for non-ASCII)Comparing values of different types with < is an error, not false:
-- print(1 < "2") --> error: attempt to compare number with stringNumbers
Section titled “Numbers”Since Lua 5.3 a number is either an integer (64-bit signed) or a float (C double). Both report type(x) == "number"; math.type tells them apart.
print(math.type(3)) -- => integerprint(math.type(3.0)) -- => floatprint(math.type(3e0)) -- => floatprint(math.type("3")) -- => nil (not a number at all)A literal is an integer if it has no decimal point and no exponent. Arithmetic preserves the distinction, with two important exceptions:
print(7 // 2) -- => 3 floor division, stays integerprint(7 / 2) -- => 3.5 '/' ALWAYS produces a floatprint(4 / 2) -- => 2.0 even when exactprint(2 ^ 10) -- => 1024.0 '^' ALWAYS produces a floatprint(7 % 2) -- => 1print(-7 // 2) -- => -4 floor, not truncationprint(-7 % 2) -- => 1 result takes the sign of the divisorMixing an integer and a float promotes to float. Equality compares mathematical value, so 1 == 1.0 is true — but tostring distinguishes them:
print(1 == 1.0) -- => trueprint(tostring(1)) -- => 1print(tostring(1.0)) -- => 1.0print(string.format("%d", 3.0)) -- => 3 (3.0 has an exact integer value)Integers wrap around on overflow; floats go to infinity:
print(math.maxinteger + 1 == math.mininteger) -- => trueprint(math.huge, -math.huge) -- => inf -infprint(1/0) -- => inf (float division by zero is fine)-- print(1//0) --> error: attempt to perform 'n//0'Convert deliberately:
print(math.tointeger(3.0)) -- => 3print(math.tointeger(3.5)) -- => nil (fails)print(math.floor(3.7)) -- => 3 (returns an integer)print(tonumber("0x1f")) -- => 31print(tonumber("ff", 16)) -- => 255print(tonumber("nope")) -- => nilLiteral forms: 42, 0xFF, 3.0, 1e-3, 0x1p4 (hex float, = 16.0).
Bitwise operators (5.3+)
Section titled “Bitwise operators (5.3+)”& | ~ << >> and unary ~ operate on 64-bit integers. Floats with exact integer values are converted; anything else is an error.
print(0xF0 & 0x3C) -- => 48print(1 << 10) -- => 1024print(~0) -- => -1Shifts are logical, not arithmetic: -1 >> 1 fills with zeros and gives a large positive number. Shifting by 64 or more yields 0.
Strings
Section titled “Strings”Strings are immutable byte sequences. They are not character sequences — #s is a byte count, and Lua has no built-in Unicode string type. Lua 5.3+ ships a small utf8 library for encoding and decoding, but indexing is still byte-based.
local s = "hello"print(#s) -- => 5print(s .. " world") -- => hello worldprint(s:upper()) -- => HELLO (method syntax works on any string)print(("%d items"):format(3)) -- => 3 itemsThe s:upper() form works because every string shares a metatable whose __index is the string library — so s:upper() is string.upper(s).
Quoting forms:
local a = 'single'local b = "double" -- identical; no interpolation in eitherlocal c = [[long string: keeps newlines,ignores a leading newline, anddoes no escape processing: \n stays literal]]local d = [==[ can contain ]] safely ]==]Escape sequences in quoted strings: \n \t \r \\ \" \' \a \b \f \v, \ddd (up to 3 decimal digits), \xXX (hex, 5.2+), \u{XXXX} (UTF-8 codepoint, 5.3+), and \z (skip following whitespace including newlines, 5.2+).
local sql = "SELECT *\z FROM t" -- => "SELECT *FROM t"print("\u{2603}") -- => a snowman, encoded as UTF-8Numbers and strings coerce in arithmetic, and numbers coerce in concatenation:
print("10" + 5) -- => 15print(10 .. 20) -- => 1020 (a string)Relying on string→number coercion is a bad habit; it hides bugs and behaves differently across versions. Use tonumber explicitly.
Because strings are immutable, building one in a loop with .. is O(n²). Collect pieces in a table and use table.concat:
local parts = {}for i = 1, 1000 do parts[#parts + 1] = tostring(i) endlocal s = table.concat(parts, ",")Multiple assignment
Section titled “Multiple assignment”Lua assigns lists to lists. Extra values are discarded, missing ones become nil.
local a, b, c = 1, 2print(a, b, c) -- => 1 2 nil
local x, y = 1, 2x, y = y, x -- swap, no temporary neededprint(x, y) -- => 2 1The whole right-hand side is evaluated before any assignment, which is what makes the swap work.
Key points
Section titled “Key points”- Eight types, and
type()returns exactly one of eight strings. - Assignment without
localcreates a global, which is a key in_ENV/_Gshared by the whole process. Default tolocal. nilmeans absent; assigningnildeletes a table key;nilcannot be a key.- Only
nilandfalseare falsy —0and""are true. - Lua 5.3+ numbers are integer or float.
/and^always give floats;//gives floor division. Lua 5.1/LuaJIT have doubles only. - Strings are immutable bytes; use
table.concatto build them in bulk.