luaguides

table.remove()

table.remove(list [, pos])

Overview

table.remove deletes an element from a Lua sequence and returns it. The default call removes the last element. With a position argument, it removes that element and shifts every trailing element down by one to keep the sequence contiguous. It is the standard way to shrink a sequence-style table, and it pairs naturally with table.insert for stack and queue work.

table.remove(list [, pos])

Parameters

NameTypeDefaultDescription
listtablerequiredA table holding a proper sequence (integer keys 1..n with no holes). The function reads #list for the default position. Non-integer keys are ignored.
posinteger#list1-based position of the element to remove. Out-of-range values return nil and leave the table unchanged.

Return value

The value that was removed. If list is empty, or if pos is out of range, the function returns nil and the table is left alone.

Worked examples

Remove the last element

local stack = {10, 20, 30, 40}
local popped = table.remove(stack)
print(popped)        --> 40
print(#stack)        --> 3
print(stack[1], stack[2], stack[3])  --> 10  20  30

The default position is #list, so popped is 40 and the table now has length 3. The trailing slot at the old end is cleared to nil so the sequence stays compact. On a single-element table, the call returns that element and leaves the table empty.

Remove from the middle

local items = {"a", "b", "c", "d", "e"}
local removed = table.remove(items, 3)
print(removed)       --> c
print(#items)        --> 4
for i, v in ipairs(items) do
  print(i, v)
end
-- output:
-- 1  a
-- 2  b
-- 3  d
-- 4  e

The element at index 3 is removed and the elements after it shift down. Notice that ipairs still iterates cleanly over the compacted sequence.

Drain a stack

local q = {1, 2, 3}
while #q > 0 do
  print(table.remove(q))
end
-- output:
-- 3
-- 2
-- 1
print(#q)            --> 0

This is the canonical LIFO pop pattern. Pair it with table.insert(t, v) to push and you have a working stack in two lines per side. Inside the loop, the position defaults to #q, so each iteration shortens the table by exactly one.

Out-of-range position is a no-op

local t = {1, 2, 3}
print(table.remove(t, 99))  --> nil
print(#t)                   --> 3

In Lua 5.3 and later, out-of-range positions return nil silently. In Lua 5.1, the same call raised "position out of bounds", so portable code may want to guard with #t first.

Build a FIFO queue

local function enqueue(q, v) table.insert(q, v) end
local function dequeue(q)    return table.remove(q, 1) end

local q = {}
enqueue(q, "first"); enqueue(q, "second"); enqueue(q, "third")
print(dequeue(q))   --> first
print(dequeue(q))   --> second
print(#q)           --> 1

This works, but removing from position 1 is O(n) because every remaining element shifts down. For high-throughput queues, track a head index instead, or pick a structure better suited to the access pattern. The stacks and queues tutorial and the linked lists tutorial walk through the trade-offs.

table.remove vs t[i] = nil

These are not the same operation. t[i] = nil deletes one key and leaves a hole; it does not shift anything and it does not change #t (except at the trailing edge). table.remove(t, i) shifts the trailing elements down and updates #t, which is what you want when you want a compact sequence.

A short comparison makes the difference visible:

local t = {"a", "b", "c"}
t[2] = nil                -- leaves a hole where "b" was
for i, v in ipairs(t) do
  print(i, v)
end
-- 1  a    (ipairs stops at the first hole)

local u = {"a", "b", "c"}
table.remove(u, 2)        -- compacts the sequence
for i, v in ipairs(u) do
  print(i, v)
end
-- 1  a
-- 2  c    (the whole sequence is reachable)

After t[2] = nil, the value at index 2 is gone, but the slot still exists as a hole, so ipairs stops the moment it hits it. After table.remove(u, 2), the remaining entries are re-packed into a clean sequence that ipairs walks end to end.

For dictionaries or sets where order does not matter, plain t[key] = nil is faster and clearer. Reach for table.remove only when the sequence ordering matters to you.

Edge cases and gotchas

Holes break #t. A table with a nil between non-nil entries has an indeterminate length in Lua 5.4, and so the default position is unreliable. Use rawlen if you need a defined length, or rebuild the table with ipairs before removing.

table.remove cannot delete nil. Tables do not store nil values, so a nil entry is a missing key, not a value to remove. If you think you need to “remove a nil”, the caller probably has a hole rather than a value.

Front-removal is O(n). Removing from the first position shifts every other element. For queues under load, use a head index.

Position is coerced like a table index. Pass integers. table.remove(t, 2.0) works as expected, table.remove(t, 2.5) is not 2.

The call is not deep. Nested tables and metatables are left alone.

Removing by value

table.remove only takes a position. To drop the first entry equal to some value, find the index first and then remove:

for i, v in ipairs(t) do
  if v == needle then
    table.remove(t, i)
    break
  end
end

ipairs stops at the first hole, so this only works on well-formed sequences. For a multi-match filter, iterate in reverse with a numeric for loop so each index stays valid after the removal.

Performance and patterns

table.remove has different cost profiles depending on the position:

  • Removing the last element is O(1) — a length read and a single erase.
  • Removing from position pos is O(n) for any pos other than #t — every trailing element shifts down.
  • The default call table.remove(t) is the fast path, because pos is #t and there is nothing to shift.

For stacks, table.remove(t) is the natural pop. For queues, table.remove(t, 1) is O(n) and a head index is faster:

local function dequeue(q)
  if q.head > #q then return nil end
  local v = q[q.head]
  q[q.head] = nil
  q.head = q.head + 1
  return v
end

This avoids the shift entirely. The trade-off is that the underlying table still occupies memory until you clear it; if the queue churns, call table.move(q, q.head, #q, 1) periodically to re-pack the live entries to the front.

See also