Part of Learn Lua with NeoVim

Lua for Beginners: String Creation, .., # Length & string.upper/lower | Episode 4

Sandy LaneSandy Lane

Video: Lua for Beginners: String Creation, .., # Length & string.upper/lower | Episode 4 by Taught by Celeste AI - AI Coding Coach

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

Lua Strings: Creation, Length, and Built-ins

Double or single quotes. .. for concatenation, # for length, string.upper/lower for case, [[ ]] for multi-line. Build a name-badge generator.

Strings in Lua are simple by design: immutable, byte-oriented, with a handful of essential operations covered by the string standard library. Today we cover the basics and build a small name-badge generator.

Two ways to quote

local first = "Alice"
local last = 'Smith'

Both produce identical strings. Pick the one that doesn't conflict with the content:

  • Use "..." if the string contains apostrophes ("don't").
  • Use '...' if the string contains double quotes ('she said "hi"').

Or escape with backslash: "don\'t" works but reads awkwardly.

Concatenation with ..

local first = "Alice"
local last = "Smith"

local full = first .. " " .. last
print(full)   -- "Alice Smith"

Use .., not +. The space is just another string literal joined into the chain.

For many concatenations (10+), use table.concat:

local parts = {"a", "b", "c", "d"}
print(table.concat(parts, ", "))  -- "a, b, c, d"

.. allocates a new string every time; for big lists it's slow. table.concat is one allocation.

Length with #

print(#full)  -- 11 (length of "Alice Smith")
print("Length: " .. #full)

The # operator returns the length in bytes, not characters. For ASCII text these are the same, but for UTF-8 text they're different — "é" is 2 bytes but 1 character.

For character-counting on UTF-8 strings, use utf8.len(s) from Lua 5.3+'s utf8 library.

Case conversion

print(string.upper(full))  -- "ALICE SMITH"
print(string.lower(full))  -- "alice smith"

string.upper and string.lower work on ASCII letters. They don't handle locale-specific cases (Turkish dotted/dotless I, German ß, etc.) — for that, use a Unicode library.

Multi-line strings with [[ ]]

local message = [[
This is a long string.
It can span multiple lines.
No need to escape "quotes" inside.
]]
print(message)

Double square brackets create a long string. Anything between them — including newlines and quotes — is preserved verbatim. No escapes needed.

If the content has ]], use a level: [==[ ... ]==]. The number of = signs lets you nest different levels safely.

A common quirk: [[ immediately followed by a newline skips that first newline. So:

local s = [[
hello]]

is "hello", not "\nhello". Saves you from awkward leading newlines.

string.rep — repeat a string

local border = string.rep("=", 30)
-- border is now "=============================="

Useful for ASCII art, dividers, padding.

The name-badge generator

Putting it all together:

local first = "Alice"
local last = "Smith"
local full = first .. " " .. last

print("--- Name Badge ---")
local border = string.rep("=", 30)
print(border)
print("  Name: " .. string.upper(full))
print("  Length: " .. #full .. " characters")
print(border)

Output:

--- Name Badge ---
==============================
  Name: ALICE SMITH
  Length: 11 characters
==============================

Five lines of Lua, a small but real-feeling tool.

Strings are immutable

local s = "Hello"
s = s .. " World"   -- creates a new string, rebinds s

You can't modify a string in place. s = s .. " World" allocates a new string and updates s to point to it. The old "Hello" becomes garbage and gets collected.

This is fine for normal use, but watch out in tight loops:

-- BAD: O(n²) memory
local result = ""
for i = 1, 1000 do result = result .. i end

-- GOOD: O(n) memory
local parts = {}
for i = 1, 1000 do parts[#parts + 1] = tostring(i) end
local result = table.concat(parts)

The naive version reallocates the whole string every iteration.

string.sub for substrings

local s = "Hello, World"
print(string.sub(s, 1, 5))    -- "Hello" (first to fifth char)
print(string.sub(s, 8))       -- "World" (from eighth to end)
print(string.sub(s, -5))      -- "World" (last 5 chars)

Lua strings are 1-indexed (not 0-indexed like C/Python). string.sub(s, i, j) returns the substring from index i to j. Negative indices count from the end.

Method-style calls

print(string.upper(s))
print(s:upper())          -- same thing

s:upper() is sugar for string.upper(s) — pass s as the first argument. The colon syntax (covered in episode 30) makes string code look more object-oriented.

Common stumbles

Mixing .. with arithmetic. "sum: " .. 5 + 3 works (8 then "sum: 8"), but "sum: " .. -5 confuses some parsers. Add parentheses when in doubt.

Forgetting # is bytes, not characters. #"é" is 2. Use utf8.len for character count.

Concatenating in a loop. O(n²) memory blow-up. Use a table + table.concat.

Using string.upper for non-ASCII. Doesn't handle locale-specific cases. For full Unicode case-folding, reach for a library.

Forgetting strings are 1-indexed. string.sub(s, 0, 5) is treated as (1, 5) because Lua clamps 0 to 1. Be explicit.

What's next

Episode 5: booleans, nil, and comparisons. The "truthy gotcha" — why 0 and "" are truthy in Lua, unlike most other languages.

Recap

Strings: "..." or '...', multi-line [[ ... ]]. Concatenate with ..; for many strings, build a table and table.concat. #s for length (in bytes). string.upper/lower for case. string.sub(s, i, j) for substrings (1-indexed; negative indices from end). string.rep(s, n) for repetition. Strings are immutable — modifications return new strings.

Next episode: booleans and the truthy gotcha.

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.