Lua with Neovim: Tables as Dictionaries — Key-Value Pairs, Dot & Bracket Notation | Episode 19
Video: Lua with Neovim: Tables as Dictionaries — Key-Value Pairs, Dot & Bracket Notation | Episode 19 by Taught by Celeste AI - AI Coding Coach
Lua Tables as Dictionaries: Key-Value Pairs
Same
{}syntax withkey = valueinstead of justvalue. Access witht.key(dot) ort["key"](bracket). Add, modify, and delete keys. The data shape behind every Lua "object."
Tables in Lua are unified — the same construct works as both an array (numeric keys) and a dictionary (string keys). Today we use the dictionary side, the foundation of records, configs, and (later) objects.
A person record
local person = {
name = "Alice",
age = 30,
city = "Portland",
}
print("Name: " .. person.name)
print("Age: " .. person.age)
print("City: " .. person["city"])
Output:
Name: Alice
Age: 30
City: Portland
Inside {}:
name = "Alice"is shorthand for["name"] = "Alice"— a string-keyed entry.- The literal
name(no quotes) becomes the key"name"(a string).
Two ways to read a value:
- Dot notation:
person.name— concise, but only works for string keys that are valid identifiers. - Bracket notation:
person["name"]— the general form. Works for any key.
Adding new keys
person.email = "alice@example.com"
print("Email: " .. person.email)
Just assign. The key is created on the spot. No "schema" to define, no class to declare.
Modifying values
person.age = 31
print("Updated age: " .. person.age)
Reassign the same key. The old value is replaced.
Deleting a key
person.city = nil
print("City after delete: " .. tostring(person.city)) -- "nil"
Setting a key to nil removes it from the table. Reading it later returns nil.
(Same as for arrays — but for dictionaries, this is the correct way to remove a key. There's no table.remove for string keys.)
Dynamic key access
local key = "language"
print(person[key])
Bracket notation lets the key come from a variable or expression:
local fields = {"name", "age", "city"}
for _, f in ipairs(fields) do
print(f .. ": " .. tostring(person[f]))
end
Iterates a list of field names and reads each from the person.
Dot notation can't do this — person.f would look for the literal key "f", not the value of variable f.
Keys with weird characters
local config = {
["max-width"] = 1200,
["api-key"] = "abc123",
}
print(config["max-width"])
-- print(config.max-width) -- error! parsed as config.max - width
Hyphens, spaces, special chars require bracket notation with a string. Same for keys that start with a digit.
A config table
local config = {
theme = "dark",
font_size = 14,
show_line_numbers = true,
language = "en",
}
print("Theme: " .. config.theme)
print("Line numbers: " .. tostring(config.show_line_numbers))
Configs are the canonical use case — a flat key-value structure where every "field" has a sensible name. Many Lua libraries pass configs as tables: setup({ ... }).
Non-string keys
Tables accept any non-nil value as a key:
local t = {}
t[1] = "one" -- integer key (array-style)
t["two"] = 2 -- string key (dict-style)
t[true] = "yes" -- boolean key
t[function() end] = "fn" -- function key (rare!)
In practice, you use integer keys for arrays, string keys for dicts/objects. Other keys are rare and usually a smell.
Iterating
for key, value in pairs(person) do
print(key .. " = " .. tostring(value))
end
pairs(t) walks every key-value pair, in unspecified order. We cover this in detail next episode.
For arrays specifically (integer keys 1..n), use ipairs(t) — guaranteed in-order, stops at first nil.
Existence check
if person.email then
print("Has email")
else
print("No email")
end
if person.email works because nil is falsy and nil is what missing keys return. But:
person.flag = false
if person.flag then -- false! treats "exists but false" as missing
print("flag set")
end
To distinguish "exists with falsy value" from "doesn't exist," compare to nil:
if person.flag ~= nil then
print("flag exists")
end
Deep dive: table is one type
local mixed = {
"first", -- key 1 (array-style)
"second", -- key 2
name = "thing", -- key "name" (dict-style)
[42] = "deep", -- key 42
}
print(mixed[1]) -- "first"
print(mixed.name) -- "thing"
print(mixed[42]) -- "deep"
print(#mixed) -- 2 (only counts contiguous integer keys from 1)
Lua doesn't distinguish "array tables" from "dict tables" — they're all just tables. Use whichever keys make sense.
The internal representation has an array part (for integer keys 1..n) and a hash part (for everything else), but that's invisible at the language level.
Common stumbles
Trying dot notation with a variable. t.key looks up the literal key "key". Use t[key].
Hyphen in a key. t.max-width is (t.max) - width. Use t["max-width"].
Treating nil as "exists but empty." A nil value means the key isn't there at all. To "track existence with a value," store false or some sentinel, and check with ~= nil.
Iteration order in pairs. Not specified. Don't rely on insertion order. For predictable order, sort the keys explicitly.
Setting a key while iterating. Adding new keys during pairs is undefined. Build a list of pending changes and apply after.
What's next
Episode 20: iterating tables. pairs for any table, ipairs for arrays, the difference, and the gap behaviour. Build a word-frequency counter.
Recap
Tables as dictionaries: { name = "Alice", age = 30 }. Read with dot (t.name) or bracket (t["name"]). Bracket is required for variable keys, special characters, or non-string keys. Add by assigning a new key. Delete by setting to nil. Tables don't distinguish array vs dict; use the same construct with integer keys (array-style) or string keys (dict-style) as fits.
Next episode: pairs and ipairs.