Posted on January 23, 2026
Table primer: or how I stopped worrying and learned to love Lua tables
So look. I get it. Tables can be scary, especially if you’re not already a coder. But you really shouldn’t keep avoiding them. The dirty secret in Lua (and thus, Mudlet) is that it’s really just tables all the way down. If you set a global variable using target = "Bob" then congrats, you just made an entry in a table whether you knew it or not. So now that you know you’re already a veteran table user, let’s pull back the covers and get a look at what you can do with them.
Congratulations, it’s fraternal twins!
I’m just going to go ahead and spill the beans. One of Lua’s dark secrets is tables are actually multiple data types hiding in a trench coat. In this post we’ll focus on the two basic ones, but for more on some of the advanced usages you might want to take a look at the Targeted Tables post.
The first type of table is often referred to as ‘indexed’ or ‘array style’ tables. These tables have entries that are referenced using a contiguous number sequence. Which is a fancy way of saying the first thing in the myTable is myTable[1], the second thing is myTable[2], and so on with no gaps in the count.
The second type of table is often referred to as ‘keyed’ or ‘map style’ tables. These tables have entries which are retrieved using a specific key. For instance, if you want to know how much gold Bob has you might store and retrieve that information in bobTable[“gold”], their silver in bobTable[“silver”] etc.
If you’re familiar with coding in other languages, these line up pretty well with actual arrays/lists and maps/hashes from those other languages. If you’re not, that’s fine I’m about to go into more detail on each of them.
Indexed / Array tables.
The first type of table we’ll look as is the indexed tabled. They’re similar to arrays and lists in other languages, in that they hold … well, a list. The index you use to reference the items in the list isn’t necessarily related to the items themselves. Think of it like a row of small lockers, numbered 1 through N, where N is the number of items you need to store in the lockers. You put one item in each locker, and then when you need to reference the item you use the number of the locker it was placed in. If you need to add an item, you put another locker at the end of the line and N goes up by 1. If you remove an item, you remove the locker it is in and the number on all the lockers after it goes down by 1. To loop through all the items in such a table, you should use the ipairs function. Let’s break it down by task. If you want to know how many items are in the table, use #myTable
Indexed Tables: Removing items
The simplest way to remove an item from a table (and retrieve it at the same time) is to use the table.remove function. The signature for the function is table.remove(myTable, positionToRemove). If you call it without the positionToRemove argument, then it will remove the last item in myTable. It’s easiest to just demonstrate this.
local tbl = { "orange", "apple", "kiwi", "banana", "strawberry" }
local someFruit = table.remove(tbl)
-- tbl is now { "orange", "apple", "kiwi", "banana" }
-- someFruit is now "strawberry"
local someOtherFruit = table.remove(tbl, 2)
-- tbl is now { "orange", "kiwi", "banana" }
-- someFruit is still "strawberry"
-- someOtherFruit is now "apple"
Indexed Tables: Adding Items
Adding an item to an indexed table is similar to removing one, but you use table.insert rather than table.remove. The signature for this function is table.insert(myTable, [position], item) with position being an optional argument. If you do not provide a position, it will add the item at the end of the table (index myTable[#+1] ). If you do provide a position, it will insert it a new locker with that item in it, and then renumber all the lockers shifted up to be correct. Here, I’ll show you.
local tbl = { "orange", "kiwi", "banana" }
table.insert(tbl, "strawberry")
-- tbl is now { "orange", "kiwi", "banana", "strawberry" }
-- and the new item "strawberry" is accessible as tbl[4]
-- this call could also be rewritten as tbl[#tbl + 1] = "strawberry" and you may see it this way frequently as well for adding items to the end of an indexed table
table.insert(tbl, 2, "apple")
-- tbl is now { "orange", "apple", "kiwi", "banana", "strawberry" }
-- and the new item "apple" is accessible as tbl[2].
-- "strawberry" is now tbl[5], as it got shifted up to make room for "apple" in locker 2
Indexed Tables: How Many Entries?
This is pretty easy for indexed tables. Just use #tbl
local tbl = { "orange", "apple" }
-- #tbl is 2
tbl[#tbl + 1] = "kiwi"
-- tbl is now { "orange", "apple", "kiwi" }
-- #tbl is 3
Indexed Tables: looping
The recommended way for looping indexed tables is to use the ipairs function. Its signature is ipairs(tbl) but you’ll pretty much exclusively use it as the basis for a for loop. Let’s take a look.
local tbl = { "orange", "apple", "kiwi" }
for index, value in ipairs(tbl) do
print(index)
print(": ")
print(value)
print("\n")
-- we could use string.format or the .. concatenation operator here but
-- I didn't want to confuse the issue
end
-- prints the following
-- 1: orange
-- 2: apple
-- 3: kiwi
To break this down, the line for index, value in ipairs(tbl) do initializes the loop. It will go through each item in numerical order (so first 1, then 2, etc) until it runs out of lockers to check in. For each locker it will put the locker number in the local variable index and the item in the locker into value. These names are arbitrary, and you can use whatever you like. So if you’re looping through a list of fruit as I was above you could use for index, fruit in ipairs(tbl) do . If you don’t intend to do anything with the index, the convention is to use _ as the variable name, for example for _, fruit in iapirs(tbl) do is valid, and indicates to anyone reading it that you’re ignoring the index in the loop. This does mean you will run into some loops in the wild like for i,v in ipairs(tbl) do but I do not recommend you do this yourself, it makes the code harder to read. I only mention it to aid you in reading the code of others.
Indexed Tables: Sorting
Since indexed tables hold their order, you might find you want to sort them. The easiest way to sort an indexed table is using the table.sort function. Its signature is table.sort(tbl, [functionToUseForSorting]) . The function to use while sorting is optional and if it isn’t provided it will sort the items in the table using < > sorting (alphanumerically for strings, numerically for numbers, and probably error or give you a surprise with anything else. Supplying your own function allows you to sort things other than just lists of strings or numbers. I’ll provide a couple examples without and one with a function.
local tbl = { "orange", "apple", "kiwi" }
table.sort(tbl)
-- tbl is now { "apple", "kiwi", "orange" }
local tbl2 = { 3, 9, 1 }
table.sort(tbl2)
-- tbl2 is now { 1, 3, 9 }
-- with a function to reverse sort
-- table.sort passes 2 items to the function you give it, and the function should return true if a should come before b, and false if b should come before a.
local function reverseIt(a, b)
return a > b
end
table.sort(tbl, reverseIt)
-- tbl is now { "orange", "kiwi", "apple" }
table.sort(tbl2, reverseIt)
-- tbl2 is now { 9, 3, 1 }
Maps/key-value Tables
These tables are nice for collecting things you want to look up by what they are, but do not need any particular order to be maintained. For instance, if you’re collecting information on other players, you might make a table per player that contains information like their name, class, and citizenship among other things. If you have this information stored in the bob table then you might reference their name as bob["name"] or bob.name . These are both the same thing, and generally speaking it’s easier to use the latter unless there’s a space in the key. For instance, you would have to reference the are they evil key as bob["are they evil"] but in practice you’d probably just stuff it in bob.evil. Let’s break it down like we did the other.
Keyed Tables: Removing Items
To remove an item from a keyed table, you just set it to nil. Super easy.
local bob = { name = "Bob the Great", class = "Wizard", citizenship = "Bobtopia" }
bob.citizenship = nil
-- bob is now { name = "Bob the Great", class = "Wizard" }
-- bob has been booted from Bobtopia
Keyed Tables: Adding Items
To add an item to a keyed table you just set it using =.
local bob = { name = "Bob the Great", class = "Wizard" }
bob.citizenship = "Bobtopia"
-- bob is now { name = "Bob the Great", class = "Wizard", citizenship = "Bobtopia" }
-- bob has returned to the glorious state of Bobtopia
-- the same thing can be used to changed an item
bob.citizenship = "CliffTown"
-- bob is now { name = "Bob the Great", class = "Wizard", citizenship = "CliffTown" }
-- bob has been moved to CliffTown
Keyed Tables: How Many Entries?
Unfortunately keyed tables do not work using #bob . Thankfully, Mudlet ships with table.size(tbl) which will return the number of items in the table.
local bob = { name = "Bob the Great", class = "Wizard", citizenship = "Bobtopia" }
-- table.size(bob) returns 3
bob.gold = 42069
-- table.size(bob) returns 4
Keyed Tables: looping
Unlike with indexed tables, ipairs will not work. Ipairs just goes up from 1 and adds 1 to it each time until your table comes up empty. Since keyed tables aren’t required to have numbers as keys, let alone in contiguous order, you have to reach instead to the pairs function. It will loop through each item in your table much like ipairs does, but it does not guarantee any sort of order. Each time you run a table through pairs it is possible it will display in a different order. In stock Lua, this is where you’d be stuck. But Mudlet ships with another looping function, spairs. I’ll demonstrate both below.
local bob = { name = "Bob the Great", class = "Wizard", citizenship = "Bobtopia" }
for key, value in pairs(bob) do
print(key)
print(": ")
print(value)
print("\n")
end
-- might print
-- name: Bob the Great
-- class: Wizard
-- citizenship: Bobtopia
-- but also might print
-- citizenship: Bobtopia
-- name: Bob the Great
-- class: Wizard
--enter spairs! its signature is spairs(tbl, functionToSortWith) but by default it behaves like table.sort.
for key, value in spairs(bob) do
print(key)
print(": ")
print(value)
print("\n")
end
-- will print, every time:
-- citizenship: Bobtopia
-- class: Wizard
-- name: Bob the Great
-- To do a reverse printing like the reverse sorting above we have to do it a little different and pass the table we're sorting as well. For example
local function reverseIt(tbl, a, b)
return tbl[a] > tbl[b]
end
for key, value in spairs(bob, reverseIt) do
print(key)
print(": ")
print(value)
print("\n")
end
-- will print, every time:
-- name: Bob the Great
-- class: Wizard
-- citizenship: Bobtopia
For some more advanced examples, check out the sorting functions from the SortBox in the MDK.
Keyed Tables: Sorting
There is no way to sort a keyed table in place. You can sort it when you go to display it using spairs as I show above, or use another table to store the order you wish to traverse the keys in for arbitrary sorting at loop time. Here’s an example.
local sortOrder = { name, citizenship, class }
-- neither alphabetical or reverse alphabetical
local bob = { name = "Bob the Great", class = "Wizard", citizenship = "Bobtopia" }
for _, keyname in ipairs(sortOrder) do -- loop the sortOrder table in a predictable fashion
print(keyname) -- the keyname we're printing now
print(": ")
print(bob[keyname]) -- we use the [] way of accessing it here since we don't want bob["keyname"] but rather bob["name"] or bob["class"] etc.
print("\n")
end
-- this will reliable print the following:
-- name: Bob the Great
-- citizenship: Bobtopia
-- class: Wizard
Summary
Whew. OK, I know that was kind of a lot, but hopefully it did a good job of showing the differences between the two main table types found in Lua, and how to interact with them. Here’s a quick summary/reference.
Indexed Tables:
- Are good for lists of things, like a grocery, shopping, or packing list
- Have their order preserved
- Have their size reported by #tbl
- Are accessed using numbers from 1 to #tbl. IE tbl[1], tbl[2], etc
- Can be looped in their preserved order using the ipairs function
- Has items added by table.insert and removed by table.remove
- Can be sorted in place (with the order preserved) using table.sort
Keyed Tables:
- Are good for referencing specific information about something. For instance, a table of information about a person, or of configuration items and their values, things like that
- Do not have their order preserved
- Do not have their size reported by #tbl, you must use table.size or count them yourself in a pairs loop instead.
- Are accessed using a key, such as “gold” or “fontSize”. IE bob.gold or bob[“gold”], config.fontSize
- Can be looped without preserving any kind of order using the pairs function
- Can be looped in a sorted manner using the ipairs function
- Has items added by simply assigning them using =, IE
bob.gold = 42069 - Has items removed by assigning
nilto them. IEbob.gold = nil - Cannot be sorted in place at all.
As always I’m happy to answer additional questions on the Mudlet discord, or wherever you might happen to run into me. I hope this has helped at least some of you come to grips with what tables are, why you might want to use one, and how to interact with them when you do.
Happy MU*ing!
