Lua syntax and types

Locals versus globals, the eight value types, 1-based tables, and the operators that surprise newcomers.

Values, locals and globals

Lua is dynamically typed and compiles to a small register-based bytecode. There is no declaration syntax beyond local: assigning to a name that has no local binding writes into the global table _G.

-- local is the default choice: it is faster and confined to the block
local name = "Lua"
local version = 5.4

-- no "local" means a real global, shared by every module in the state
counter = 0

print(type(name))        -- string
print(10 / 4)            -- 2.5    division always produces a float
print(10 // 4)           -- 2      floor division, Lua 5.3+
print(2 ^ 10)            -- 1024.0
print(#"hello")          -- 5      length operator
print("a" .. "b")        -- ab     .. concatenates, .. is not "or"
print(1 == 1.0)          -- true   integer and float compare numerically
print(2 ~= 3)            -- true   "not equal" is ~=, never !=
  • Numbers have two subtypes since 5.3: 64-bit integers and 64-bit floats. math.type(3) is "integer".
  • Strings are immutable and 8-bit clean, so they hold arbitrary bytes as well as UTF-8 text.
  • Everything is a first-class value: functions can be stored in tables, passed and returned.
  • Names are case sensitive, cannot start with a digit, and cannot be a reserved word such as end or local.

Tables are the only structure

There are eight types in total — nil, boolean, number, string, function, table, thread and userdata — and table covers arrays, records, objects, modules and namespaces. Array-like parts are indexed from 1.

TypeExampleWhat it is
nilnilAbsence of a value; the only value that is falsy besides false
booleantrue, falseNo implicit conversion from numbers or strings
number42, 3.14Integer or float subtype, both 64-bit
string"hi", 'hi'Immutable byte sequence, concatenated with ..
table{ 1, 2, k = 3 }The one composite type; keys may be any value except nil
functionfunction() endClosures with lexical scoping
threadcoroutine.create(f)A coroutine; not an OS thread
userdataio.stdoutOpaque host (C) data exposed through methods
local t = { "a", "b", "c" }        -- array part
print(t[1], #t)                    -- a  3     index 1 is the first element

local record = { id = 1, tags = { "x" } }
record.name = "Ada"                -- sugar for record["name"]
record[1] = "first"                -- positional key, unrelated to id

-- ipairs walks the array part until the first nil; pairs walks every key
for i, v in ipairs(t) do print(i, v) end
for k, v in pairs(record) do print(k, v) end

table.insert(t, "d")               -- append
table.insert(t, 1, "z")            -- shift everything right
table.remove(t, 1)                 -- and back
print(table.concat(t, ","))        -- a,b,c,d
print(#({ 1, 2, nil, 4 }))         -- undefined: a table with holes has no reliable length

Control flow and functions

local function classify(n)
  if n == nil then return "missing" end
  if n % 2 == 0 then return "even" elseif n > 100 then return "big odd" end
  return "odd"
end

-- repeat runs the body before testing, so it always executes once
local tries = 0
repeat tries = tries + 1 until tries >= 3

-- numeric for: start, limit, optional step
for i = 10, 1, -3 do io.write(i, " ") end    -- 10 7 4 1
print()

-- varargs and multiple returns
local function stats(...)
  local count, sum = select("#", ...), 0
  for _, v in ipairs({ ... }) do sum = sum + v end
  return count, sum          -- returning a list, not a table
end
local n, total = stats(2, 4, 6)
print(n, total)              -- 3  12

-- a table constructor keeps all return values in the last position
print({ stats(1, 2) })       -- a two-element table
print({ stats(1, 2), 9 })    -- a single-element table

Functions are values, so callbacks and modules are just tables of functions. local function binds the name before the body, which is what lets the function call itself recursively.

⚠️
Only false and nil are falsy. if 0 then and if "" then both take the true branch, so test lengths and counts explicitly rather than relying on truthiness the way you would in Python or JavaScript.

FAQ

Should I always declare locals?
Yes. Locals are resolved at compile time into registers, while globals need a table lookup on every access; they are also confined to the block, which stops accidental cross-module coupling. Most style guides add local everywhere and expose a single return table.
Why does my array print garbage keys?
You probably used pairs on an array, which has no defined order. Use ipairs for sequences and pairs for records, and never mix the two meanings on one table.

Tables, metatables and object orientation Python: getting started

Last refreshed 2026-09-18.