</>
CodeLearn Pro
Start Learning Free โ†’

Lua โ€” Complete Beginner Tutorial

6 modules ยท 17 examples ยท Click โ–ถ Run Program to see the console output

โœฆ Beginnerโ† Overview

๐Ÿ’ก How to use this tutorial

Read each explanation, study the code, then click โ–ถ Run Program to see the output.

Module 01

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.

script.luaLua
-- 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))
โ–ถ Click Run Program to see output

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().

script.luaLua
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)))
โ–ถ Click Run Program to see output

.. 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 %.

script.luaLua
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 10
โ–ถ Click Run Program to see output

In 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.

Module 02

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.

script.luaLua
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.")
end
โ–ถ Click Run Program to see output

In 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.

script.luaLua
-- 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)
end
โ–ถ Click Run Program to see output

Lua 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.

Module 03

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().

script.luaLua
-- 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))
โ–ถ Click Run Program to see output

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.

script.luaLua
-- 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)
end
โ–ถ Click Run Program to see output

The (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.

script.luaLua
-- 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")
โ–ถ Click Run Program to see output

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.

Module 04

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.

script.luaLua
-- 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, ", "))
โ–ถ Click Run Program to see output

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.

script.luaLua
-- 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)
โ–ถ Click Run Program to see output

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.

script.luaLua
-- 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))
โ–ถ Click Run Program to see output

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.

Module 05

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.

script.luaLua
-- 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)
โ–ถ Click Run Program to see output

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.

script.luaLua
-- 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())
โ–ถ Click Run Program to see output

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.

Module 06

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.

script.luaLua
-- 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)
โ–ถ Click Run Program to see output

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.

script.luaLua
-- 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))
โ–ถ Click Run Program to see output

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.