Arrays and Lists in Lua

· 7 min read · Updated March 26, 2026 · beginner
tables arrays data-structures table.insert table.remove

Lua ships without a dedicated array type. Instead, the language leans on a single data structure for almost everything: the table. Tables are associative arrays — they map keys to values — but when those keys are consecutive integers starting at 1, they behave exactly like the arrays and lists you find in Python, JavaScript, or Java.

This tutorial shows you how to use Lua tables as arrays. You’ll learn how 1-based indexing works, why the # operator can surprise you, how to use table.insert, table.remove, and table.concat, and how to tell a sequential table from a sparse one. You’ll finish by building a stack, a queue, and a deque — all from plain tables.

How Lua Tables Become Arrays

A table in Lua is a dictionary. You can store any value under any key:

local person = { name = "Maya", age = 31 }
print(person["name"])  -- Maya

For an array, you drop the string keys and use consecutive integers instead:

local primes = { 2, 3, 5, 7, 11 }
print(primes[1])  -- 2
print(primes[3])  -- 5

That is an array. There is no separate type — it is just a table with integer keys. You can mix both styles in the same table, though that quickly stops looking like an array:

local hybrid = { "first", "second", third = "III" }
print(hybrid[1])        -- first
print(hybrid["third"]) -- III

For the rest of this tutorial, the focus stays on the array-like pattern.

1-Based Indexing

Most mainstream languages start array indices at 0. Lua does not — it starts at 1. The first element is always at index 1, the second at index 2, and so on.

local months = { "January", "February", "March", "April" }

for i = 1, #months do
    print(i, months[i])
end

Output:

1    January
2    February
3    March
4    April

This choice comes from Lua’s origins in scientific and numerical computing, where 1-based indexing is the norm (MATLAB, Fortran, Mathematica all start at 1). It also makes range loops read cleanly: for i = 1, #arr iterates from the first element to the last with no awkward offset. The trade-off is that code ported from C, Python, or Java needs adjustment when accessing the first element.

The Length Operator

The unary # operator returns the length of a table. For a sequential table with no gaps, it behaves as expected:

local colours = { "red", "green", "blue" }
print(#colours)  -- 3

The Gap Problem

The # operator is defined only for sequential tables — those where every integer key from 1 to n is present and non-nil. If a table has a hole, the result is unpredictable:

local broken = { "a", "b", nil, "d", "e" }
print(#broken)  -- implementation-defined: may print 2, 4, or something else

The same applies to tables that have non-integer keys mixed in:

local messy = { "x", "y", "z" }
messy["label"] = "end"
print(#messy)  -- still 3, but be cautious with mixed tables

If you are unsure whether a table has gaps, count manually:

local function table_size(t)
    local count = 0
    for _ in pairs(t) do
        count = count + 1
    end
    return count
end

local sparser = {}
sparser[1] = "one"
sparser[3] = "three"  -- index 2 is nil
print(table_size(sparser))  -- 2, regardless of the gap

The practical rule: never set an array element to nil to “remove” it. Use table.remove instead, which shifts subsequent elements down and closes the gap.

Core Table Functions

Lua’s standard library provides three functions that make array-style tables practical: table.insert, table.remove, and table.concat. All three assume sequential, gap-free tables.

table.insert

table.insert(t, value) appends value to the end of t. table.insert(t, pos, value) inserts value at a specific index, shifting everything from pos onward one step to the right:

local list = { 1, 2, 3 }

table.insert(list, 4)        -- { 1, 2, 3, 4 }
table.insert(list, 2, 99)     -- { 1, 99, 2, 3, 4 }

table.remove

table.remove(t) removes and returns the last element. table.remove(t, pos) removes and returns the element at pos, shifting everything after it one step to the left:

local list = { 10, 20, 30, 40 }

local last = table.remove(list)
print(last)  -- 40, list is now { 10, 20, 30 }

local mid = table.remove(list, 2)
print(mid)   -- 20, list is now { 10, 30 }

table.concat

table.concat(t) joins all elements of t into a string. Pass a separator as the second argument, and optionally a start and end index:

local words = { "Hello", "world", "from", "Lua" }

print(table.concat(words, " "))           -- Hello world from Lua
print(table.concat(words, ", ", 2, 3))    -- world, from

Sequential Tables vs Sparse Tables

A sequential table (sometimes called a dense array) has integer keys forming an unbroken chain from 1 to n. Every position is filled:

local sequential = { "first", "second", "third" }
-- Keys: 1, 2, 3 — no gaps

A sparse table has gaps or uses unusual keys:

local sparse = {}
sparse[1] = "one"
sparse[3] = "three"   -- index 2 is absent
sparse[10] = "ten"    -- large gap between 3 and 10

Sparse tables are valid Lua, but the # operator gives undefined results for them, and ipairs() stops at the first nil gap. Only use pairs() when iterating over sparse tables — it visits every key-value pair regardless of gaps.

-- Using pairs for sparse tables
for k, v in pairs(sparse) do
    print(k, v)
end
-- May print: 1 one, 3 three, 10 ten (order not guaranteed)

Most of the time, you want sequential tables for array-style work.

Building a Stack

A stack follows last-in, first-out (LIFO) order. table.insert pushes onto the top, and table.remove pops from it:

local function new_stack()
    return {}
end

local function push(stack, item)
    table.insert(stack, item)
end

local function pop(stack)
    return table.remove(stack)
end

local stack = new_stack()
push(stack, "draft")
push(stack, "review")
push(stack, "publish")

while #stack > 0 do
    print(pop(stack))
end

Output:

publish
review
draft

Because table.remove without an index targets the last element, popping from a stack is a single operation. There is no need to track a top-of-stack pointer manually.

Building a Queue

A queue follows first-in, first-out (FIFO) order. The catch is that table.remove(t, 1) shifts every element down by one position, making it O(n) for the dequeue operation. For moderate workloads this is fine; for performance-critical queue-heavy code, a different approach using numeric indices is needed.

local function new_queue()
    return {}
end

local function enqueue(queue, item)
    table.insert(queue, item)
end

local function dequeue(queue)
    return table.remove(queue, 1)
end

local q = new_queue()
enqueue(q, "first")
enqueue(q, "second")
enqueue(q, "third")

while #q > 0 do
    print(dequeue(q))
end

Output:

first
second
third

Building a Deque

A deque (double-ended queue) lets you add and remove from both ends. Lua’s table.insert and table.remove handle both cases — table.insert(t, 1, value) inserts at the front, and table.remove(t, 1) removes from the front:

local function new_deque()
    return {}
end

local function push_right(deque, item)
    table.insert(deque, item)
end

local function push_left(deque, item)
    table.insert(deque, 1, item)
end

local function pop_right(deque)
    return table.remove(deque)
end

local function pop_left(deque)
    return table.remove(deque, 1)
end

local deque = new_deque()
push_right(deque, "A")
push_right(deque, "B")
push_left(deque, "Z")
print(pop_right(deque))  -- B
print(pop_left(deque))   -- Z
print(pop_right(deque))  -- A

A Complete Example: Filtering Scores

To tie the pieces together, here is a runnable script that builds an array, filters it, and computes a summary using table.insert, table.remove, and table.concat:

local raw_scores = { 72, 95, 88, 61, 99, 74, 85, 91 }

-- Remove scores below 75 (replace with nil, then compact)
for i = 1, #raw_scores do
    if raw_scores[i] < 75 then
        raw_scores[i] = nil
    end
end

-- Compact: rebuild as a sequential table
local passing = {}
for _, score in pairs(raw_scores) do
    table.insert(passing, score)
end

-- Sort and format
table.sort(passing, function(a, b) return a > b end)

print("Passing scores (sorted):", table.concat(passing, ", "))

local sum = 0
for i = 1, #passing do
    sum = sum + passing[i]
end
print("Average:", string.format("%.1f", sum / #passing))

Output:

Passing scores (sorted): 99, 95, 91, 88, 85, 74
Average: 88.7

Notice the pattern: removing elements by setting them to nil creates gaps, so table.sort would behave unpredictably. Rebuilding into a fresh sequential table with pairs sidesteps that problem entirely.

Summary

Lua’s tables are deceptively simple. On the surface they look like ordinary arrays, but underneath they are full hash maps that can hold any mix of keys and values. For array-style work, stick to sequential tables — integer keys from 1 to n with no gaps — and let table.insert, table.remove, and table.concat do the heavy lifting. Reserve pairs() for sparse or mixed tables, and never trust # on a table that might have holes.