Programming languages cheat sheetLua logo

Lua cheat sheet

Lua syntax on one page, grouped by what you are trying to do: tables, string patterns, metatables, modules and coroutines, checked against Lua 5.5.

Last updated

Lua is a small, fast scripting language built to be embedded in other programs, which is why you meet it inside games, editors and servers more often than on its own. It has one data structure, the table, and a handful of ideas that stretch a long way: functions as values, metatables and coroutines. The reference below is grouped by what you are trying to do, and the filter box searches all of it at once. Type table and everything about tables comes to you, or type 5.5 to see what the latest release added.

Every snippet is checked against Lua 5.5, the current version from lua.org, and was run on the 5.5.1 interpreter built from the official source. Anything newer than 5.3 says so in the notes column, and Where Lua runs covers the older dialects in Roblox, Neovim and LÖVE. Names like user and nums are placeholders for your own. Coming from another language? The Ruby, PHP, Swift and Dart cheat sheets are grouped the same way, so they read side by side.

Running Lua and LuaRocks

TaskCommandNotes
Check which version you havelua -v
Run a scriptlua script.lua
Open an interactive shellluaCtrl+D to leave
Run a script, then stay in the shelllua -i script.lua
Run one line without a filelua -e 'print(_VERSION)'
Load a module before runninglua -l inspect script.lua
Check a file for syntax errorsluac -p file.lua
Turn on warn() messageslua -W script.lua
Where require looks for .lua filesLUA_PATH="./lib/?.lua;;" lua app.luaThe ;; keeps the default path on the end
Install a packageluarocks install inspectLuaRocks is installed separately from Lua
Install into your home directoryluarocks install --local penlight
Let Lua find --local packageseval "$(luarocks path)"Add it to your shell profile
List installed packagesluarocks list
Show what a package isluarocks show inspect
Remove a packageluarocks remove inspect

Lua is a tiny C library plus a small standalone interpreter, so system package managers often ship an older version under a name like lua5.4. The source from lua.org builds in seconds with make.

Variables and types

TaskCodeNotes
Print a lineprint("Hello")Several arguments are separated by tabs
Local variablelocal name = "Ada"Always use local unless you mean a global
Global variablecount = 0Any name you never declared local is global
Constantlocal MAX <const> = 100Lua 5.4+. Assigning to it is a compile error
Declare the globals a file may setglobal<const> * global configLua 5.5+. Any other global name is then read-only
Assign several at oncelocal a, b = 1, 2
Swap two variablesa, b = b, a
Comment-- one line
Block comment--[[ several lines ]]
What type is ittype(value)Returns a string: "nil", "number", "string", "table", "function"...
Integer or floatmath.type(3) math.type(3.0)"integer" and "float". Lua 5.3+
Is it nilvalue == nil
Not equala ~= bNot !=
Default when nil or falselocal name = input or "Anonymous"
Pick one of two valueslocal label = n == 1 and "item" or "items"Breaks if the middle value is false or nil
Divide7 / 2Always a float: 3.5
Floor division7 // 23. Rounds down, so -7 // 2 is -4. Lua 5.3+
Remainder7 % 3Takes the sign of the divisor: -7 % 3 is 2
Raise to a power2 ^ 10Always a float: 1024.0
Bitwise and, or, xor, nota & b a | b a ~ b ~aLua 5.3+. Integers only
Shift bits1 << 4 256 >> 4
String to numbertonumber("42")nil when it is not a number
Parse in another basetonumber("ff", 16)
Number to stringtostring(42)
Float to integer, if it is wholemath.tointeger(3.0)nil for 3.5
Round down or upmath.floor(3.7) math.ceil(3.2)
Largest and smallestmath.max(1, 5, 3) math.min(2, 8)
Random whole number from 1 to 6math.random(1, 6)
Biggest integer, infinitymath.maxinteger math.hugeIntegers wrap around on overflow

Only nil and false are falsy. 0 and the empty string are true, which catches out anyone coming from JavaScript, Python or C. There is no separate integer type in older Lua: 5.1, LuaJIT and Luau have one number type, a double.

Strings

Every string function lives in the string table, and strings look their methods up there, so s:upper() and string.upper(s) are the same call. Positions start at 1 and ranges include both ends. Negative positions count from the end.

TaskCodeNotes
Join stringsfirst .. " " .. lastNumbers join too: 10 .. "" is "10"
Length in bytes#sBytes, not characters: #"café" is 5
Length in charactersutf8.len("café")4. Lua 5.3+
Change cases:upper() s:lower()
Part of a strings:sub(1, 5) s:sub(-3)From position 1 to 5 inclusive, and the last 3
Find plain texts:find("cat", 1, true)Returns start and end, or nil. The true turns patterns off
Find a patterns:find("%d+")
Pull out a matchs:match("%d+")The matched text, or nil
Pull out several partslocal y, m, d = date:match("(%d+)-(%d+)-(%d+)")One value per capture
Replace every matchs:gsub("cat", "dog")Returns the new string and a count
Replace the first match onlys:gsub("cat", "dog", 1)
Reuse a capture in the replacements:gsub("(%w+)=(%w+)", "%2=%1")
Loop over every matchfor word in s:gmatch("%a+") do ... end
Split on commasfor part in s:gmatch("[^,]+") do ... endSkips empty fields. Use "([^,]*)" to keep them
Trim whitespaces:match("^%s*(.-)%s*$")
Does the whole string matchs:match("^%d+$") ~= nil
Format with placeholdersstring.format("%s has %d items", name, n)
Format a decimalstring.format("%.2f", price)
Pad with zerosstring.format("%03d", 7)007
Quote a string safelystring.format("%q", s)Output can be read back by Lua
Repeats:rep(3) s:rep(3, ", ")The second form puts a separator between copies
Reverses:reverse()
Character codess:byte(1) string.char(72, 105)
String across several lineslocal text = [[ ... ]]No escapes are processed inside
Newline and tab"line\n" "col\t"
Build a long string in a loopparts[#parts + 1] = piece table.concat(parts, ", ")Much faster than .. in a loop

Lua patterns are not regular expressions. %d is a digit, %a a letter, %w a letter or digit, %s whitespace, %u upper case, and . any character. - is the lazy version of *. There is no | for alternatives and no {n} counts. Escape the magic characters ( ) . % + - * ? [ ] ^ $ with %.

Tables as arrays

The table is Lua's only data structure. An array is a table whose keys are 1, 2, 3 and so on, with no gaps. Every function here assumes that shape.

TaskCodeNotes
Create an arraylocal nums = {10, 20, 30}
Read by positionnums[1] nums[#nums]Indexes start at 1. nums[0] is nil
How many items#numsOnly reliable with no nil gaps
Appendnums[#nums + 1] = 40Or table.insert(nums, 40)
Insert at a positiontable.insert(nums, 1, 5)Shifts the rest up
Remove the last itemtable.remove(nums)Returns the removed item
Remove at a positiontable.remove(nums, 1)Shifts the rest down
Loop with the positionfor i, v in ipairs(nums) do ... endStops at the first nil
Loop backwardsfor i = #nums, 1, -1 do ... endSafe for removing while you loop
Sorttable.sort(nums)In place, returns nothing
Sort with your own ordertable.sort(people, function(a, b) return a.age < b.age end)Return true when a goes first
Join into a stringtable.concat(words, ", ")
Spread into argumentsmath.max(table.unpack(nums))
Pack arguments with a countlocal args = table.pack(...)args.n counts nils too
Copy an arraylocal copy = table.move(nums, 1, #nums, 1, {})
Preallocate a big arraylocal t = table.create(1000)Lua 5.5+. Still empty, just sized
Array of arrayslocal grid = {{1, 2}, {3, 4}} grid[2][1]
Find an item's positionfor i, v in ipairs(list) do if v == x then return i end endNo built-in indexOf

Tables as maps

The same table type works as a dictionary. Keys can be any value except nil and NaN, and a key set to nil is removed.

TaskCodeNotes
Create with string keyslocal user = {name = "Ada", age = 36}
Key that is not a plain name{["first name"] = "Ada", [42] = "answer"}
Read a valueuser.name user["name"]nil when the key is missing
Read with a variable keyuser[key]user.key means the literal key "key"
Set a valueuser.email = "ada@example.com"
Remove a keyuser.age = nil
Loop over pairsfor key, value in pairs(user) do ... endOrder is not defined
Loop in key orderlocal keys = {} for k in pairs(t) do keys[#keys + 1] = k end table.sort(keys)
Is the table emptynext(t) == nil#t is 0 for any table with no array part
Count the keyslocal n = 0 for _ in pairs(t) do n = n + 1 endNo built-in count
Read a nested value safelylocal host = cfg.db and cfg.db.hostNo ?. operator
Count how often each value appearscounts[w] = (counts[w] or 0) + 1
Use as a setseen[item] = true if seen[item] then ... end
Fill in defaultsfor k, v in pairs(defaults) do if opts[k] == nil then opts[k] = v end end
Shallow copylocal copy = {} for k, v in pairs(t) do copy[k] = v end
Compare two tablesa == bOnly true for the same table, not equal contents

Control flow

TaskCodeNotes
If, elseif, elseif a then ... elseif b then ... else ... endelseif is one word
And, or, nota and b a or b not aReturn one of their operands, not just true or false
While loopwhile i < 10 do ... end
Loop at least oncerepeat ... until doneuntil can see locals from inside the loop
Count upfor i = 1, 10 do ... endBoth ends included
Count in stepsfor i = 10, 1, -1 do ... end
Loop over an arrayfor i, v in ipairs(list) do ... end
Loop over a tablefor k, v in pairs(t) do ... end
Leave a loopbreak
Skip to the next iterationgoto continue ::continue::No continue keyword. Put the label at the end of the loop body
Change the loop variablelocal i = iLua 5.5 makes the for variable read-only, so shadow it first
Return earlyif not user then return end
Return from the middle of a blockdo return endreturn must be the last statement in a block

Functions and closures

TaskCodeNotes
Define a functionlocal function add(a, b) return a + b end
Anonymous functionlocal square = function(x) return x * x end
Return several valuesreturn min, maxlocal lo, hi = bounds(list)
Keep only the first result(bounds(list))Extra parentheses cut it to one value
Default parametername = name or "world"Missing arguments arrive as nil
Any number of argumentslocal function sum(...) end
Arguments as a tablelocal args = {...}
Count arguments, nils includedselect("#", ...)
Name the extra argumentslocal function log(...args) print(args.n) endLua 5.5+. args is read-only
Pass arguments straight onstring.format(fmt, ...)
Named argumentsconnect{host = "db", port = 5432}One table argument, no parentheses needed
Closure with private statelocal function counter() local n = 0 return function() n = n + 1 return n end end
Method callobj:move(1, 2)Same as obj.move(obj, 1, 2)
Define a methodfunction Point:move(dx, dy) self.x = self.x + dx endThe colon adds a self parameter
Recursive local functionlocal function fact(n) if n <= 1 then return 1 end return n * fact(n - 1) endlocal function lets it see its own name
Pass a function as a valuetable.sort(list, compare)

Errors

TaskCodeNotes
Raise an errorerror("something broke")Adds the file and line to the message
Blame the caller's lineerror("expected a number", 2)
Raise without a positionerror("bad input", 0)
Raise a tableerror({code = 404})Handlers get the table back unchanged
Check a conditionassert(file, "config missing")Returns its arguments when they pass
Catch an errorlocal ok, err = pcall(risky, arg1, arg2)ok is false and err is the message
Catch with a tracebackxpcall(risky, debug.traceback)
Function that can fail softlyreturn nil, "not found"The usual style in the standard library
Handle a soft failurelocal f, err = io.open(path) if not f then print(err) end
Clean up however the block endslocal f <close> = assert(io.open(path))Lua 5.4+. Needs a __close metamethod, which files have

Lua has no try and catch. pcall runs a function in protected mode and turns an error into a return value. Library functions that can fail for ordinary reasons, like io.open, return nil and a message instead of raising.

Metatables and objects

A metatable is a table of hooks that changes how another table behaves: what happens when a key is missing, what + means, how it prints. Classes in Lua are built from the __index hook.

TaskCodeNotes
Attach a metatablesetmetatable(t, mt)Returns t
Read a metatablegetmetatable(t)
Fall back to another tablesetmetatable(t, {__index = defaults})Only for keys missing from t
Compute missing values__index = function(t, key) return ... end
Intercept new keys__newindex = function(t, key, value) rawset(t, key, value) endOnly runs for keys not already in t
Skip the metatablerawget(t, k) rawset(t, k, v) rawequal(a, b)
Operators__add __sub __mul __div __mod __unm __concata + b calls __add(a, b)
Comparisons__eq __lt __le
What tostring and print show__tostring = function(v) return "(" .. v.x .. ")" end
What # returns__len = function(v) return v.size end
Call a table like a function__call = function(self, arg) end
Run code when a variable goes out of scope__close = function(v, err) endLua 5.4+. For local x <close>
Weak keys, for a cachesetmetatable(cache, {__mode = "k"})"v" for weak values
Stop changes to the metatable__metatable = "locked"
Class with a constructorlocal Point = {} Point.__index = PointThen setmetatable({x = x}, Point) in Point.new
Inherit from a classlocal Cat = setmetatable({}, {__index = Animal}) Cat.__index = Cat

Modules

TaskCodeNotes
Load a modulelocal json = require("json")Runs json.lua once, then returns the cached result
Load from a subfolderrequire("app.util")Finds app/util.lua
Write a modulelocal M = {} function M.hello() end return M
Keep a helper privatelocal function helper() endOnly what you put in the returned table is public
Where require lookspackage.path
Add your own folderpackage.path = "./lib/?.lua;" .. package.path
Force a reloadpackage.loaded.util = nilThen require again
Try a module that may be missinglocal ok, lib = pcall(require, "lfs")
Run a file every timedofile("config.lua")No caching, unlike require
Compile a string of codelocal fn = load("return 1 + 1")Returns nil and an error on bad syntax
Run code in a sandboxload(code, "user", "t", {print = print})The table is the only global scope it can see

Coroutines

A coroutine is a function you can pause with yield and continue with resume. Only one runs at a time, so there are no threads or locks: it is how game scripts wait across frames and how generators keep their place.

TaskCodeNotes
Create a coroutinelocal co = coroutine.create(function(a) ... end)It does not start yet
Start or continue itlocal ok, value = coroutine.resume(co, 1)Arguments go in, yielded values come out
Pause and hand back a valuelocal reply = coroutine.yield(value)reply is what the next resume passes in
Is it running, paused or donecoroutine.status(co)"suspended", "running", "normal" or "dead"
Wrap it as a plain functionlocal next_id = coroutine.wrap(function() ... end)Errors are raised instead of returned
Use it as a for-loop iteratorfor n in coroutine.wrap(gen) do ... endThe loop ends when it returns
Is the current code inside a coroutinecoroutine.isyieldable()
Close a paused coroutinecoroutine.close(co)Lua 5.4+. Runs pending __close handlers

resume never raises. If the coroutine errors, resume returns false and the message, and the coroutine is dead from then on.

A class, start to finish

Most of the metatable reference in one place: a constructor, methods with :, __tostring and __lt, a method that fails softly with nil, message, and a subclass that inherits through __index.

local Account = {}
Account.__index = Account
 
function Account.new(owner, balance)
  local self = setmetatable({}, Account)
  self.owner = owner
  self.balance = balance or 0
  return self
end
 
function Account:deposit(amount)
  if amount <= 0 then
    error("deposit must be positive", 2)
  end
  self.balance = self.balance + amount
  return self
end
 
function Account:withdraw(amount)
  if amount > self.balance then
    return nil, "insufficient funds"
  end
  self.balance = self.balance - amount
  return self.balance
end
 
Account.__tostring = function(a)
  return string.format("%s: %.2f", a.owner, a.balance)
end
 
Account.__lt = function(a, b) return a.balance < b.balance end
 
-- A subclass: look up missing methods on Account.
local Savings = setmetatable({}, { __index = Account })
Savings.__index = Savings
Savings.__tostring = Account.__tostring
Savings.__lt = Account.__lt
 
function Savings.new(owner, balance, rate)
  local self = Account.new(owner, balance)
  self.rate = rate
  return setmetatable(self, Savings)
end
 
function Savings:add_interest()
  return self:deposit(self.balance * self.rate)
end
 
local ada = Account.new("Ada", 100):deposit(50)
local alan = Savings.new("Alan", 200, 0.05):add_interest()
 
print(ada)                         --> Ada: 150.00
print(alan)                        --> Alan: 210.00
print(ada < alan)                  --> true
print(ada:withdraw(500))           --> nil  insufficient funds
print(pcall(ada.deposit, ada, -5)) --> false  deposit must be positive

Metamethods such as __tostring and __lt are looked up on the metatable itself, not through __index, which is why Savings copies them from Account. Methods like deposit are ordinary keys, so they are found through the __index chain.

A module, start to finish

A module is a file that returns a table. require finds it on package.path, runs it once and caches the result, so every file that requires it shares the same table.

-- greeter.lua
local M = {}
 
local default_name = "world" -- private: not in M
 
function M.hello(name)
  return "Hello, " .. (name or default_name)
end
 
function M.shout(name)
  return M.hello(name):upper() .. "!"
end
 
return M
-- main.lua, in the same folder
local greeter = require("greeter")
 
print(greeter.hello())      --> Hello, world
print(greeter.shout("Ada")) --> HELLO, ADA!

require("greeter.util") turns the dots into folder separators and looks for greeter/util.lua. Packages from LuaRocks land on the same search path, so they load the same way. Commit your own modules with the rest of the project; the Git cheat sheet covers that workflow.

Patterns, start to finish

Lua's patterns are smaller than regular expressions but cover most text jobs. Here they pick apart a log: captures in gmatch, a nested gmatch for key=value pairs, and the two ways to match a literal $ or ..

local log = [[
2026-09-26 12:01:07 INFO  user=ada action=login
2026-09-26 12:03:44 ERROR user=alan action=upload size=12MB
2026-09-26 12:04:02 INFO  user=ada action=logout
]]
 
local counts = {}
for date, time, level, rest in log:gmatch("(%d+%-%d+%-%d+) (%d+:%d+:%d+) (%u+)%s+([^\n]*)") do
  counts[level] = (counts[level] or 0) + 1
 
  -- Pull key=value pairs out of the rest of the line into a table.
  local fields = {}
  for key, value in rest:gmatch("(%w+)=(%w+)") do
    fields[key] = value
  end
 
  if level == "ERROR" then
    print(time, fields.user, fields.action) --> 12:03:44  alan  upload
  end
end
 
print(counts.INFO, counts.ERROR) --> 2  1
 
-- Escape the magic characters ( ) . % + - * ? [ ] ^ $ with %.
local price = "Total: $4.50"
print(price:match("%$(%d+%.%d+)")) --> 4.50
 
-- Or search for plain text with the fourth argument to find.
print(price:find("$4.", 1, true)) --> 8  10

Coroutines, start to finish

A coroutine keeps its local variables and its place between calls. Wrapped with coroutine.wrap, it becomes an iterator you can hand straight to a for loop, and iterators can be chained into a pipeline.

-- A generator: yields values one at a time, keeps its place between calls.
local function range(from, to, step)
  return coroutine.wrap(function()
    for i = from, to, step or 1 do
      coroutine.yield(i)
    end
  end)
end
 
for n in range(10, 30, 10) do
  print(n) --> 10, 20, 30
end
 
-- A pipeline: each stage pulls from the one before it.
local function lines(text)
  return coroutine.wrap(function()
    for line in text:gmatch("[^\n]+") do
      coroutine.yield(line)
    end
  end)
end
 
local function non_blank(source)
  return coroutine.wrap(function()
    for line in source do
      if line:find("%S") then
        coroutine.yield(line)
      end
    end
  end)
end
 
for line in non_blank(lines("alpha\n   \nbeta\n")) do
  print(line) --> alpha, beta
end
 
-- Passing values both ways with resume and yield.
local acc = coroutine.create(function(first)
  local total = first
  while true do
    local n = coroutine.yield(total)
    total = total + n
  end
end)
 
print(coroutine.resume(acc, 5))  --> true  5
print(coroutine.resume(acc, 10)) --> true  15
print(coroutine.status(acc))     --> suspended

Declaring globals in Lua 5.5

A misspelt variable name in Lua silently creates a new global. Lua 5.5 adds the global keyword to stop that. Once a file declares any global, every name it uses must be declared, and global<const> * covers the built-ins such as print and string as read-only.

-- Lua 5.5: from here on, every name must be declared.
-- Existing globals such as print and string can be read but not reassigned.
global<const> *
 
-- Globals this file is allowed to set.
global config, VERSION
 
VERSION = "1.2.0"
config = { debug = false }
 
local function load_settings()
  cofnig = { debug = true } -- typo: compile error, not a silent new global
end

The typo is caught when the file is compiled, before anything runs: attempt to assign to const variable 'cofnig'. Without the global<const> * line, global config on its own would also stop print from being found, because nothing declared it. None of this exists before 5.5, so leave it out of code that has to run in Neovim, LÖVE or Roblox.

Where Lua runs

Most people write Lua inside another program, and most of those programs run an older or modified Lua. The table below is the short version. Everything on this page about tables, functions, closures, metatables and coroutines works in all of them. The version notes in the reference tell you what does not.

WhereLanguageWhat is different from Lua 5.5
RobloxLuau, derived from Lua 5.1Type annotations, continue, +=, backtick strings, const x = 1. No goto, no integers, no & or | (use bit32)
NeovimLuaJIT, Lua 5.1 plus some extrasNo //, no bitwise operators (use bit), no integers, no utf8, no <const>
LÖVE (games)LuaJIT by defaultSame as Neovim
Redis scripts, World of Warcraft add-onsLua 5.1Same limits as LuaJIT, plus no goto

Roblox

Roblox games are scripted in Luau, which Roblox develops from Lua 5.1. This script gives each player a coin counter on the leaderboard and tops it up every minute. It is Luau, not Lua 5.5: the : Player annotation, the backtick string, += and task.wait do not exist in standard Lua.

-- A Script in ServerScriptService. Luau, not Lua 5.5.
local Players = game:GetService("Players")
 
local STARTING_COINS = 100
 
Players.PlayerAdded:Connect(function(player: Player)
	local stats = Instance.new("Folder")
	stats.Name = "leaderstats"
	stats.Parent = player
 
	local coins = Instance.new("IntValue")
	coins.Name = "Coins"
	coins.Value = STARTING_COINS
	coins.Parent = stats
 
	print(`{player.Name} joined with {coins.Value} coins`)
end)
 
while true do
	task.wait(60)
	for _, player in Players:GetPlayers() do
		local stats = player:FindFirstChild("leaderstats")
		local coins = stats and stats:FindFirstChild("Coins")
		if coins then
			coins.Value += 10
		end
	end
end

Luau numbers are always floats, so 10 / 2 prints 5 rather than 5.0, and table.create(3, "x") fills a new array with three copies of "x", where Lua 5.5's table.create only reserves space.

Neovim

Neovim reads ~/.config/nvim/init.lua at startup and runs it with LuaJIT, so the Lua 5.1 rules apply. The editor's API lives in the global vim table. This config was loaded in Neovim 0.12; vim.hl.on_yank needs 0.11 or later.

-- ~/.config/nvim/init.lua
vim.g.mapleader = " "
 
vim.opt.number = true
vim.opt.expandtab = true
vim.opt.shiftwidth = 2
 
vim.keymap.set("n", "<leader>w", "<cmd>write<cr>", { desc = "Save the file" })
 
vim.api.nvim_create_autocmd("TextYankPost", {
  desc = "Flash the text you just yanked",
  callback = function()
    vim.hl.on_yank()
  end,
})

Game scripting with LÖVE

LÖVE is a free 2D game framework: you write main.lua, define the callbacks it looks for, and it calls them every frame. It runs LuaJIT by default, so the same 5.1 limits apply. Engines that embed Lua work the same way: the host calls your functions, and your functions call the host's API.

-- main.lua for LÖVE: run the folder with `love .`
local player = { x = 100, y = 100, speed = 200 }
 
function love.load()
  love.window.setTitle("Move the square")
end
 
function love.update(dt)
  -- dt is the seconds since the last frame, so speed is per second.
  if love.keyboard.isDown("right") then player.x = player.x + player.speed * dt end
  if love.keyboard.isDown("left") then player.x = player.x - player.speed * dt end
end
 
function love.draw()
  love.graphics.rectangle("fill", player.x, player.y, 32, 32)
end

Gotchas

The mistakes almost everyone makes in their first week of Lua.

Looks rightWhat actually happensDo this instead
count = 0 inside a functionCreates or overwrites a globallocal count = 0, or global declarations in 5.5
if n then when n is 0The branch runs: only nil and false are falsyif n ~= 0 then
if a != b thenSyntax errora ~= b
#{1, 2, nil, 4}2 or 4, depending on version and how the table was builtKeep arrays gap-free, or track the count in a field
x = cond and false or "other"Always "other", because false fails the orA plain if
s:find(".")Finds the first character: . matches anythings:find(".", 1, true) or s:find("%.")
print(s:gsub("a", "b"))Prints the string and a countprint((s:gsub("a", "b")))
obj.method()self is nil inside the methodobj:method()
for i = 1, 3 do i = i + 1 endCompile error in 5.5: the loop variable is read-onlylocal j = i and change j
s = s .. piece in a big loopBuilds a new string every time, which is slowCollect pieces in a table, then table.concat
print(0.1 + 0.2)0.30000000000000004 in 5.5, where 5.4 printed 0.3string.format("%.2f", x) for display
table.sort(t, function(a, b) return a <= b end)Can raise "invalid order function"Use <: the function must be a strict order

table.sort is also not stable: items that compare equal can come out in any order. It is a quick sort variant, unlike merge sort, which keeps equal items in place. Both are on the site as step-through visualisations.

Common questions

Which version of Lua does this cheat sheet cover?

Lua 5.5, the current version, which came out in December 2025, checked against the 5.5.1 release. Most of the page also works in Lua 5.4. Anything newer than 5.3 says so in the notes column, for example global declarations, named varargs and table.create need 5.5, and <const> and <close> need 5.4. Run lua -v to see which version you have.

Why do Lua arrays start at 1?

Lua was designed in 1993 for engineers who were not professional programmers, and counting from 1 matched how they counted. The standard library follows it throughout: #t, ipairs, table.insert, table.sort and string positions all assume the first item is at 1. You can store something at index 0, but none of those functions will see it.

What is the difference between pairs and ipairs?

ipairs walks the array part of a table in order, from 1 up to the first nil, and gives you the index and value. pairs visits every key in the table, including string keys, in no particular order. Use ipairs for lists where order matters and pairs for dictionaries or when you need every key.

What is the difference between a dot and a colon in Lua?

A colon passes the object as a hidden first argument called self. obj:move(1, 2) is the same as obj.move(obj, 1, 2), and function Point:move(dx, dy) is the same as function Point.move(self, dx, dy). Calling a method with a dot by mistake shifts every argument along by one, which usually shows up as an error about self being a number or nil.

Is Roblox Lua the same as Lua?

No. Roblox runs Luau, a language derived from Lua 5.1 and developed by Roblox. It adds optional type annotations, string interpolation with backticks, compound assignment like +=, a continue keyword and const, and it leaves out goto, the integer subtype and the bitwise operators. Everything on this page about tables, functions, metatables and coroutines carries over, but check the version notes before copying 5.3+ syntax.

Which version of Lua does Neovim use?

Neovim embeds LuaJIT, which follows Lua 5.1 with some later additions such as goto. So in an init.lua there is no // operator, no bitwise operators (use the bit module), no integer subtype, no utf8 library and no <const> or <close>. The Neovim API lives in the global vim table, for example vim.opt, vim.keymap.set and vim.api.

Why does #t give the wrong length?

The length operator is only defined for sequences, tables with values at 1 to n and no nil in between. If there is a gap, # can return the position of any border, where a non-nil value is followed by nil, so different Lua versions and even different ways of building the same table can give different answers. Keep arrays gap-free, or store the count yourself in a field such as n.

Does Lua have classes?

Not as a keyword. A class is a table of methods that is also used as the metatable of its instances, with __index pointing back at itself, so a missing key on an instance is looked up in the class. Inheritance is the same trick one level up: the subclass gets a metatable whose __index is the parent class. The worked example on this page shows the whole pattern.

See all cheat sheets

Want this explained by a cat?

The videos cover the same ground in sixty seconds. If there is a tool you want a cheat sheet for next, ask.