Part of Learn Lua with NeoVim

Learn Lua in Neovim: Tables as Arrays — 1-Based Indexing, Insert, Remove & Length | Episode 17

Sandy LaneSandy Lane

Video: Learn Lua in Neovim: Tables as Arrays — 1-Based Indexing, Insert, Remove & Length | Episode 17 by Taught by Celeste AI - AI Coding Coach

Take the quiz on the full lesson page
Test what you've read · interactive walkthrough

Lua Tables as Arrays: 1-Based Indexing, Insert, Remove

Tables are Lua's universal data structure. As arrays: { "a", "b", "c" }, indexed 1 through #t. table.insert, table.remove, #t for length. The 1-based indexing is the surprise.

Tables are the data structure in Lua. Same construct used as arrays, dictionaries, structs, objects, and modules. Today we use them as plain arrays — sequences of values indexed by integers.

Creating an array

local fruits = { "apple", "banana", "cherry", "date" }

print("First fruit: " .. fruits[1])
print("Third fruit: " .. fruits[3])
print("How many fruits: " .. #fruits)

Output:

First fruit: apple
Third fruit: cherry
How many fruits: 4

The { ... } syntax creates a table literal. Items separated by commas. As an array, the first item is at index 1.

1-based indexing

Most languages use 0-based indexing — arr[0] is the first item. Lua uses 1-based:

local fruits = { "apple", "banana", "cherry" }
print(fruits[1])   -- "apple"
print(fruits[0])   -- nil (no zeroth item)

This is the single biggest surprise for programmers coming from C, Python, JavaScript, etc. There's no way around it — 1 is "first" in Lua.

The historical reason: Lua draws on Pascal and pre-C languages where 1-based was common. It also matches the way humans count.

# for length

print(#fruits)   -- 4

The # operator returns the length — the count of items in the array part of the table.

It's defined for tables with no internal nil gaps. With gaps, # is undefined — could return any boundary:

local t = {1, 2, nil, 4, 5}
print(#t)   -- 2, 5, or anywhere in between (implementation-defined)

For arrays you treat as sequences, never nil out an element you still want to count. To "remove" an item, use table.remove.

Insert and remove

table.insert(fruits, "elderberry")
print("After insert: " .. #fruits)   -- 5
print("Last fruit: " .. fruits[#fruits])   -- "elderberry"

table.remove(fruits, 2)
print("After removing index 2: " .. #fruits)   -- 4

table.insert(t, value) appends value to the end. Equivalent to t[#t + 1] = value.

table.insert(t, pos, value) inserts at a specific position, shifting later items down.

table.remove(t, pos) removes the item at pos, shifting later items up. Returns the removed value:

local removed = table.remove(fruits, 2)
print("Removed: " .. removed)

table.remove(t) (no position) removes the last item — like pop in other languages.

Iterating

for i = 1, #fruits do
  print(i .. ". " .. fruits[i])
end

Standard for loop with #fruits as the upper bound. Reads top-to-bottom, in order.

There's also ipairs (covered in episode 20):

for i, v in ipairs(fruits) do
  print(i .. ". " .. v)
end

ipairs walks the array part starting from index 1, stopping at the first nil. Same result for a normal array.

Setting and clearing slots

fruits[5] = "fig"     -- assign
fruits[5] = nil       -- "remove" by setting to nil

Setting to nil removes the slot. But this can leave a gap that breaks #:

local t = {1, 2, 3, 4, 5}
t[3] = nil
print(#t)   -- could be 2 or 5

To shrink an array safely, use table.remove — it doesn't leave gaps.

Adding at any index

local t = {}
t[1] = "a"
t[2] = "b"
t[3] = "c"

Same as {"a", "b", "c"}. The literal is just sugar — under the hood, tables are flexible mappings from any key to any value.

t[100] = "far away"
print(t[100])   -- "far away"
print(#t)       -- 3 (the array part stops at the first nil gap)

You can index any integer; the table holds the value. But # only counts the contiguous prefix.

Mixing array and dictionary

local t = {
  "first",
  "second",
  name = "things",
}

Lua tables are flexible — you can mix array entries (indexed 1, 2) and named entries (string keys) in the same table. We dive into the dictionary side in episode 19.

Common stumbles

0-based thinking. t[0] is nil, not the first element. t[1] is first. Period.

Off-by-one in loops. for i = 0, #t - 1 skips the last element and reads t[0] (nil). Use for i = 1, #t.

Setting to nil to "remove." Leaves a gap; breaks #. Use table.remove.

Counting on # for tables with nils. Undefined. Track the count yourself or use select with variadics.

ipairs stopping at the first nil. That's the contract — ipairs walks 1, 2, 3 ... and stops when it hits a nil. For dictionary-style iteration (any key), use pairs (episode 20).

What's next

Episode 18: table array operations. table.sort, table.concat, custom sort comparators, building a small to-do list with insert(t, pos, v).

Recap

Tables are Lua's universal data structure; today we use them as arrays. 1-based indexing. { "a", "b" } literal syntax. #t for length (undefined with gaps). table.insert(t, v) to append, table.insert(t, pos, v) to insert, table.remove(t, pos) to remove and shift. Use table.remove, not t[i] = nil, when you want to shrink the sequence.

Next episode: sort, concat, and to-do lists.

Ready? Take the quiz on the full lesson page →
Test what you've learned. Watch the lesson and try the interactive quiz on the same page.