Lua โ Complete Beginner Tutorial
6 modules ยท 17 examples ยท Click โถ Run Program to see the console output
๐ก How to use this tutorial
Read each explanation, study the code, then click โถ Run Program to see the output.
Variables & Types
Lua is dynamically typed โ you never declare a type. Just assign a value and Lua figures it out. Use the local keyword for all your variables. Without local, a variable is global and can be accidentally accessed from anywhere in your program, which leads to hard-to-find bugs. Lua has eight basic types: nil, boolean, number, string, function, table, thread, and userdata.
Creating Variables with local
Always use local to declare variables. A local variable only exists inside the block where it is created. Without local, the variable becomes global โ accessible everywhere, which sounds useful but causes bugs in larger programs. Lua uses -- for single-line comments and --[[ ]] for multi-line comments.
-- local keyword makes variables safe and scoped
local name = "Alice" -- string
local age = 25 -- number
local score = 95.5 -- number (all numbers are floats)
local passing = true -- boolean
local nothing = nil -- nil means "no value"
-- print() shows values in the console
print(name)
print(age)
print(score)
print(passing)
print(nothing)
-- type() tells you the type of any value
print(type(name))
print(type(age))
print(type(passing))
print(type(nothing))nil in Lua means "nothing" or "does not exist". Every undeclared variable returns nil โ there is no error. This means typos in variable names silently give you nil instead of an error. Always use local to catch these mistakes early.
Strings & Concatenation
Strings can use single or double quotes โ both work identically. Join strings with the .. (two dots) operator. Get the length of a string with the # operator. Lua has a built-in string library with useful methods accessed with the colon syntax: string.upper() or str:upper().
local firstName = "Alice"
local lastName = 'Smith' -- single quotes work too
local greeting = "Hello"
-- Concatenation uses .. (two dots)
local fullName = firstName .. " " .. lastName
print(fullName)
print(greeting .. ", " .. firstName .. "!")
-- # gives string length
print("Length:", #fullName)
-- String methods (two ways to call them)
print(string.upper(firstName))
print(firstName:upper()) -- colon syntax (same thing)
print(firstName:lower())
print(firstName:len())
print(firstName:rep(3, "-")) -- repeat 3 times with separator
print((" hello "):match("^%s*(.-)%s*$")) -- trim whitespace
-- Number to string
local n = 42
print("The answer is: " .. tostring(n))
print(type(tostring(n))).. is the only concatenation operator in Lua. Unlike Python (+) or JavaScript (+), Lua will not auto-convert numbers to strings when you use ... You must call tostring() first, or Lua will throw an error.
Numbers & Maths
In Lua 5.2 and earlier, all numbers are 64-bit floating point. Lua 5.3+ added a separate integer type, but they behave the same for most purposes. The math library provides common mathematical functions. Integer division uses // and the modulo operator is %.
local a = 20
local b = 6
-- Basic arithmetic
print(a + b) -- 26
print(a - b) -- 14
print(a * b) -- 120
print(a / b) -- 3.3333... (always float division)
print(a // b) -- 3 (integer/floor division)
print(a % b) -- 2 (remainder)
print(a ^ 2) -- 400 (power โ uses ^ not **)
-- Math library
print(math.sqrt(144)) -- 12.0
print(math.floor(3.9)) -- 3
print(math.ceil(3.1)) -- 4
print(math.abs(-7)) -- 7
print(math.max(4,7,2,9,1)) -- 9
print(math.min(4,7,2,9,1)) -- 1
print(math.pi) -- 3.1415926535898
-- Random numbers
math.randomseed(42)
print(math.random(1, 10)) -- random int between 1 and 10In Lua, / always returns a float (3.3333...). Use // for integer division (floor division). This is different from Python 3 which also uses // but Lua's / vs // distinction is a common source of confusion for beginners.
Control Flow
Lua uses if / elseif / else / end for decisions, while / do / end and repeat / until for loops, and for / do / end for counted loops. Notice that every block ends with the end keyword โ there are no curly braces in Lua. The repeat-until loop always runs at least once, which is useful for input validation.
if / elseif / else
Lua's if statement checks a condition. If true, it runs the block. elseif (one word, not "else if") checks another condition. else runs when nothing matched. Every if block ends with end. In Lua, only false and nil are "falsy" โ even 0 and empty string "" are truthy! This is different from many other languages.
local score = 73
if score >= 90 then
print("Grade: A โ Outstanding!")
elseif score >= 80 then
print("Grade: B โ Great work!")
elseif score >= 70 then
print("Grade: C โ Good effort")
elseif score >= 60 then
print("Grade: D โ Just passed")
else
print("Grade: F โ Try again")
end
-- Truthy/falsy: only false and nil are falsy
print("--- Truthy/Falsy ---")
if 0 then print("0 is truthy!") end -- prints! (unlike C/Python)
if "" then print("Empty string is truthy!") end -- prints!
if nil then print("never") else print("nil is falsy") end
if false then print("never") else print("false is falsy") end
-- Combining conditions with and / or / not
local age = 20
local hasID = true
if age >= 18 and hasID then
print("Welcome!")
else
print("Sorry, cannot enter.")
endIn Lua, ONLY false and nil are falsy. The number 0 and the empty string "" are TRUTHY. This is the opposite of C, Python, and JavaScript where 0 and "" are falsy. This trips up experienced programmers switching to Lua.
for, while & repeat-until
Lua has three loop types. The numeric for counts from start to end with an optional step. while keeps running while a condition is true. repeat-until runs at least once then checks the condition at the bottom. Use break to exit any loop early.
-- Numeric for: for var = start, stop, step do
print("--- Counting up ---")
for i = 1, 5 do
print("Count:", i)
end
print("--- Counting down ---")
for i = 10, 1, -3 do -- step of -3
print(i)
end
print("--- while loop ---")
local count = 1
while count <= 4 do
print("While:", count)
count = count + 1 -- Lua has no ++ or +=
end
print("--- repeat-until ---")
local n = 5
repeat
print("Repeat:", n)
n = n - 1
until n <= 0 -- runs while condition is FALSE
print("--- break ---")
for i = 1, 10 do
if i == 5 then
print("Stopped at", i)
break
end
print(i)
endLua has no ++ or += operators! Use count = count + 1 or count = count - 1. This is one of the most common gotchas for beginners coming from C, Java, or JavaScript.
Functions
Functions in Lua are first-class values โ you can store them in variables, pass them to other functions, and return them. Lua functions can return multiple values at once, which is used throughout the language. Variadic functions accept any number of arguments using the ... syntax.
Defining & Calling Functions
Define functions with the function keyword. Parameters go in parentheses. Use return to send values back. If a function has no return statement, it returns nil. Functions are stored in variables โ local function name() is the same as local name = function().
-- Basic function
local function greet(name)
print("Hello, " .. name .. "!")
end
greet("Alice")
greet("Bob")
-- Function with return value
local function square(n)
return n * n
end
print("5 squared:", square(5))
print("12 squared:", square(12))
-- Default parameter workaround (Lua has no defaults)
local function greetWithTitle(name, title)
title = title or "Friend" -- "or" trick for defaults!
return "Hello, " .. title .. " " .. name .. "!"
end
print(greetWithTitle("Alice", "Dr"))
print(greetWithTitle("Bob")) -- uses default
-- Functions are values โ store them in variables
local double = function(n) return n * 2 end
print("Double 7:", double(7))
-- Pass functions as arguments
local function applyTwice(fn, value)
return fn(fn(value))
end
print("Apply double twice to 3:", applyTwice(double, 3))The "or" trick for default parameters: title = title or "Friend" works because if title is nil (not passed), the "or" expression returns "Friend". This is idiomatic Lua and you will see it everywhere.
Multiple Return Values
Lua functions can return more than one value โ just separate them with commas after return. The caller receives all the values. This is used everywhere in Lua, especially for returning a result plus an error status. No need for tuples, structs, or error exceptions.
-- Return multiple values with comma
local function divmod(a, b)
return math.floor(a / b), a % b -- quotient AND remainder
end
local q, r = divmod(17, 5)
print("17 / 5 = " .. q .. " remainder " .. r)
-- Swap two values (classic use of multiple returns)
local function swap(a, b)
return b, a
end
local x, y = 10, 20
print("Before:", x, y)
x, y = swap(x, y)
print("After:", x, y)
-- Return status + value (error handling pattern)
local function safeDivide(a, b)
if b == 0 then
return nil, "Error: cannot divide by zero"
end
return a / b, nil
end
local result, err = safeDivide(10, 2)
if err then
print("Failed:", err)
else
print("Result:", result)
end
local result2, err2 = safeDivide(10, 0)
if err2 then
print("Failed:", err2)
else
print("Result:", result2)
endThe (result, error) pattern โ returning nil plus an error message on failure โ is Lua's standard error handling idiom. You will see this everywhere in Lua code. It avoids exceptions entirely and forces the caller to check for errors.
Closures & Variadic Functions
A closure is a function that remembers variables from its surrounding scope, even after that scope has ended. Variadic functions use ... to accept any number of arguments. Both are used heavily in real Lua code.
-- Closure: inner function remembers outer variables
local function makeCounter(start)
local count = start or 0
return function() -- returns a function!
count = count + 1
return count
end
end
local counter1 = makeCounter(0)
local counter2 = makeCounter(10)
print(counter1()) -- 1
print(counter1()) -- 2
print(counter1()) -- 3
print(counter2()) -- 11 (independent counter!)
print(counter2()) -- 12
print("---")
-- Variadic function: ... accepts any number of args
local function sum(...)
local args = {...} -- pack ... into a table
local total = 0
for _, v in ipairs(args) do
total = total + v
end
return total
end
print("Sum:", sum(1, 2, 3))
print("Sum:", sum(10, 20, 30, 40, 50))
-- select('#', ...) counts the arguments
local function countArgs(...)
print("Got", select('#', ...), "arguments")
end
countArgs("a", "b", "c", "d")Each call to makeCounter() creates a NEW closure with its OWN count variable. counter1 and counter2 are completely independent even though they share the same function code. This is the power of closures.
Tables
Tables are the most important data structure in Lua โ in fact, they are the ONLY data structure. Tables can act as arrays (with integer keys starting at 1), dictionaries (with string or any other keys), objects, modules, and namespaces. Mastering tables is mastering Lua.
Tables as Arrays
When you fill a table with values in order, it acts like an array. Lua arrays index from 1 โ not 0 like most languages! Use # to get the length, ipairs() to iterate in order, and table.insert() / table.remove() to add and remove items.
-- Create a table as an array
local fruits = {"apple", "banana", "cherry", "mango"}
-- Access by index โ STARTS AT 1, NOT 0!
print(fruits[1]) -- first element
print(fruits[2]) -- second
print(fruits[4]) -- fourth
print(#fruits) -- length: 4
-- Iterate with ipairs (in order, stops at first nil)
for i, fruit in ipairs(fruits) do
print(i, fruit)
end
print("---")
-- Add to the end
table.insert(fruits, "kiwi")
print("After insert:", #fruits, fruits[5])
-- Insert at specific position
table.insert(fruits, 2, "grape")
print("After insert at 2:", fruits[2])
-- Remove from end
local removed = table.remove(fruits)
print("Removed:", removed)
-- Remove from position
table.remove(fruits, 1)
print("First item now:", fruits[1])
-- table.concat: join into a string
local numbers = {10, 20, 30, 40, 50}
print(table.concat(numbers, ", "))Lua arrays start at index 1, not 0. This is intentional โ the language designers felt 1-based indexing was more natural for humans. If you access index 0, you get nil (not an error), which can silently cause bugs.
Tables as Dictionaries
Tables can use any value as a key โ not just numbers. String keys are the most common, creating dictionary-like structures. Use dot notation (t.key) for string keys that are valid identifiers, or bracket notation (t["key"]) for anything else.
-- Table as dictionary (key-value pairs)
local student = {
name = "Alice",
age = 20,
score = 95,
city = "Cape Town",
passing = true,
}
-- Access with dot notation
print(student.name)
print(student.age)
-- Access with bracket notation (same thing)
print(student["score"])
print(student["city"])
-- Add new key
student.email = "alice@example.com"
print(student.email)
-- Check if key exists (returns nil if not)
print(student.phone) -- nil (does not exist)
-- Delete a key (set to nil)
student.passing = nil
print(student.passing) -- nil (deleted)
-- Iterate with pairs (any order)
print("--- All fields ---")
for key, value in pairs(student) do
print(key, "=", value)
end
-- Mixed table (array + dictionary parts)
local mixed = {
"first", "second", "third", -- array part: indices 1,2,3
label = "my table", -- dictionary part
count = 3,
}
print(mixed[1])
print(mixed.label)pairs() iterates ALL key-value pairs in a table but in NO guaranteed order. ipairs() iterates only the integer keys starting from 1, in order, and stops at the first nil gap. Use ipairs() for arrays and pairs() for dictionaries.
Nested Tables & table as Namespace
Tables can contain other tables โ this is how you build complex data structures. Tables also serve as modules and namespaces in Lua โ grouping related functions together under one name.
-- Nested tables
local school = {
name = "CodeLearn Academy",
students = {
{ name = "Alice", score = 95 },
{ name = "Bob", score = 82 },
{ name = "Carol", score = 71 },
},
}
print(school.name)
print(school.students[1].name)
print(school.students[2].score)
-- Loop through nested table
for i, student in ipairs(school.students) do
print(i .. ". " .. student.name .. ": " .. student.score)
end
print("---")
-- Table as a namespace/module
local MathUtils = {}
MathUtils.PI = 3.14159
function MathUtils.circleArea(r)
return MathUtils.PI * r * r
end
function MathUtils.factorial(n)
if n <= 1 then return 1 end
return n * MathUtils.factorial(n - 1)
end
print(string.format("Area: %.2f", MathUtils.circleArea(5)))
print("5! =", MathUtils.factorial(5))Using tables as namespaces (MathUtils.circleArea) is the standard way to organise Lua code. Every Lua library does this โ string.upper, math.sqrt, table.insert are all functions stored inside tables.
OOP with Metatables
Lua does not have built-in classes, but you can build them using metatables. A metatable is a regular table that controls how another table behaves โ what happens when you index it, add two tables together, call it like a function, and more. The __index metamethod is the key to implementing classes and inheritance in Lua.
Metatables & __index
When you access a key that does not exist in a table, Lua checks for a metatable. If the metatable has an __index field, Lua looks there instead. This is how you add "default" values and methods to tables โ which is how classes work in Lua.
-- A simple "class" using metatables
local Animal = {}
Animal.__index = Animal -- look up missing keys in Animal
-- Constructor function
function Animal.new(name, sound)
local self = setmetatable({}, Animal) -- create instance
self.name = name
self.sound = sound
return self
end
-- Methods go on Animal
function Animal:speak()
-- the colon (:) automatically passes self as first arg
print(self.name .. " says " .. self.sound .. "!")
end
function Animal:describe()
print("I am " .. self.name)
end
-- Create instances
local dog = Animal.new("Rex", "Woof")
local cat = Animal.new("Whiskers", "Meow")
dog:speak()
cat:speak()
dog:describe()
-- Access fields
print(dog.name)
print(cat.sound)The colon (:) in method definitions and calls is syntactic sugar. function Animal:speak() is the same as function Animal.speak(self). Calling dog:speak() is the same as dog.speak(dog). The colon automatically passes the table as the first argument.
Inheritance Pattern
You can build inheritance by chaining __index. When Lua looks up a key, it first checks the instance, then its metatable's __index, then that table's metatable's __index โ as deep as you like. This creates a prototype chain similar to JavaScript.
-- Base class
local Shape = {}
Shape.__index = Shape
function Shape.new(colour)
local self = setmetatable({}, Shape)
self.colour = colour or "white"
return self
end
function Shape:getColour()
return self.colour
end
function Shape:describe()
print("Shape โ colour: " .. self:getColour())
end
-- Derived class: Circle extends Shape
local Circle = setmetatable({}, { __index = Shape })
Circle.__index = Circle
function Circle.new(radius, colour)
-- Call parent constructor, then add own fields
local self = Shape.new(colour)
setmetatable(self, Circle)
self.radius = radius
return self
end
function Circle:area()
return math.pi * self.radius ^ 2
end
-- Override parent method
function Circle:describe()
print(string.format(
"Circle โ r=%.1f, area=%.2f, colour=%s",
self.radius, self:area(), self:getColour()
))
end
local s = Shape.new("blue")
local c = Circle.new(5, "red")
s:describe()
c:describe()
print(c:getColour()) -- inherited from Shape!
print(c:area())setmetatable(Circle, { __index = Shape }) links Circle to Shape. When Lua looks up a method in Circle and does not find it, it follows __index to Shape. This is prototype-based inheritance, the same model used by JavaScript.
Coroutines & Modules
Coroutines are one of Lua's most unique and powerful features. A coroutine is like a function that can pause itself (yield) and resume later โ without blocking anything. This makes them perfect for game state machines, co-operative multitasking, and producer-consumer patterns. Modules are simply Lua files that return a table of functions.
Coroutines โ Pause & Resume
Create a coroutine with coroutine.create(). Resume it with coroutine.resume(). The coroutine runs until it hits coroutine.yield(), then pauses and returns control to the caller. The caller can pass values in on resume, and the coroutine can pass values out on yield.
-- Create a coroutine from a function
local function countdown(from)
print("Starting countdown from " .. from)
for i = from, 1, -1 do
coroutine.yield(i) -- pause and send i back
end
return "Blast off! ๐" -- final value on completion
end
local co = coroutine.create(countdown)
-- Resume the coroutine step by step
local ok, value = coroutine.resume(co, 5) -- start with arg 5
print("Status:", ok, "Value:", value)
ok, value = coroutine.resume(co)
print("Status:", ok, "Value:", value)
ok, value = coroutine.resume(co)
print("Status:", ok, "Value:", value)
ok, value = coroutine.resume(co)
print("Status:", ok, "Value:", value)
ok, value = coroutine.resume(co)
print("Status:", ok, "Value:", value)
-- After the function returns:
ok, value = coroutine.resume(co)
print("Final:", ok, "Value:", value)
-- Dead coroutine โ cannot resume again
ok, value = coroutine.resume(co)
print("Dead:", ok, "Message:", value)coroutine.resume() returns true plus any values passed to yield (or returned when done). If the coroutine errors, it returns false plus the error message. Always check the first return value before using the second one.
Modules with require()
A Lua module is a file that returns a table. You load it with require(). The module table contains all the public functions and values. This is how Lua's standard libraries work โ string, math, table, io are all tables loaded automatically. Here we simulate a module inline.
-- Simulating a module (normally this would be a .lua file)
-- math_utils.lua would contain:
local MathUtils = {}
function MathUtils.clamp(value, min, max)
if value < min then return min end
if value > max then return max end
return value
end
function MathUtils.lerp(a, b, t)
return a + (b - a) * t
end
function MathUtils.round(n, decimals)
local factor = 10 ^ (decimals or 0)
return math.floor(n * factor + 0.5) / factor
end
function MathUtils.average(t)
local sum = 0
for _, v in ipairs(t) do sum = sum + v end
return sum / #t
end
-- Use the module
print(MathUtils.clamp(150, 0, 100)) -- 100 (clamped)
print(MathUtils.clamp(-5, 0, 100)) -- 0 (clamped)
print(MathUtils.clamp(50, 0, 100)) -- 50 (unchanged)
print(MathUtils.lerp(0, 100, 0.25)) -- 25.0 (25% between)
print(MathUtils.lerp(0, 100, 0.75)) -- 75.0
print(MathUtils.round(3.14159, 2)) -- 3.14
print(MathUtils.round(3.14159, 4)) -- 3.1416
local scores = {75, 88, 92, 61, 95}
print("Average:", MathUtils.average(scores))Real modules are separate .lua files that end with "return ModuleTable". Load them with require("math_utils") โ Lua caches the module after the first load so require() is fast. This is how every Lua project organises code across files.
You finished the Lua tutorial!
You can now write Lua scripts with variables, control flow, functions, tables, metatables for OOP, coroutines, and modules. These are the skills used in every real Lua project โ from Roblox games to embedded system scripts.