luaguides

string.lower

string.lower(s)

string.lower does one thing: it returns a new string with every uppercase letter folded to lowercase. Everything else (digits, punctuation, accented bytes, NUL) passes through unchanged. That sentence is the whole API, but the C-locale and UTF-8 details underneath it trip up almost everyone who uses it on real text.

Synopsis

string.lower(s)

Returns a new string. Does not mutate s.

Parameters

sstring, required. The input to convert. Must be a string; Lua does not auto-coerce numbers, so string.lower(42) errors with bad argument #1 to 'lower' (string expected, got number). If the value might not be a string, coerce it first with string.lower(tostring(x)).

Return Value

A new string. Strings are immutable in Lua, so the original s is never touched. The result is a fresh copy, not a view.

local s = "Hello"
local t = string.lower(s)
print(s)  --> Hello
print(t)  --> hello

string.lower("") returns "", which makes the function safe to use as an identity-style default in higher-order code (for example, as the seed value for a string.gsub replacement table).

Examples

Basic conversion

print(string.lower("HELLO"))            --> hello
print(string.lower("Lua 5.4"))          --> lua 5.4
print(string.lower("MiXeD cAsE 123!"))   --> mixed case 123!

The function leaves the input alone — it never modifies the original. Calling it without capturing the result is a common bug that catches beginners who expect in-place mutation, since Lua strings are immutable and the function returns a brand-new value:

local s = "Hello"
string.lower(s)         -- return value discarded
print(s)                --> Hello   (not "hello")

s = string.lower(s)     -- reassign to use the new string
print(s)                --> hello

Case-insensitive comparison

Lowering both sides is the simplest way to compare strings without caring about case. The function accepts any string-typed input, so you can pass user input, file contents, or message payloads directly. For data that already meets a known case convention, this is overkill — for untrusted or mixed-source input, though, it is the cleanest way to fold case sensitivity out of the comparison:

local function ci_eq(a, b)
  return string.lower(a) == string.lower(b)
end

print(ci_eq("Lua", "LUA"))     --> true
print(ci_eq("Lua", "LuaJIT"))  --> false

This works for ASCII. If your inputs contain non-ASCII letters, the comparison becomes locale-dependent — see the section below.

Case-insensitive sorting

Pass a comparator to table.sort that lowers both operands:

local names = {"bob", "Alice", "cara", "Dave"}
table.sort(names, function(a, b)
  return string.lower(a) < string.lower(b)
end)
-- names is now {"Alice", "bob", "cara", "Dave"}

The lowering is a bit of wasted work per comparison, but for small lists it is the clearest approach. For larger lists, lowercase once into a parallel array, sort the indices, and project back — that turns an O(n log n) lowercasing cost into O(n).

Locale and character set

string.lower is a thin wrapper over the C library’s tolower, so the result depends on the program’s current C locale. On the default "C" / "POSIX" locale, only ASCII AZ (bytes 0x410x5A) become lowercase. With a locale like "en_US.UTF-8" or "pt_BR.iso88591" set on the host, single-byte Latin-1 uppercase letters (such as Á, Ç, Ü) also convert.

This matters because Lua exposes no API to read or change the locale from pure Lua — it inherits whatever the host C program set. If a script runs under setlocale(LC_ALL, ""), behavior can shift between machines. Pin a locale explicitly on the host side, or restrict inputs to ASCII.

UTF-8 Caveats

string.lower operates byte by byte. A two-byte UTF-8 uppercase code point like Á (bytes 0xC3 0x81) gets each byte lowered independently, producing mojibake rather than á. This is the single most common complaint on the Lua mailing list.

For UTF-8 text, use the utf8 library that ships with Lua 5.3+:

local utf8 = require("utf8")
print(utf8.lower("CAFÉ"))   --> café

utf8.lower understands code points and uses Unicode default case folding, which is what most users actually want. Roblox (Luau) users should reach for the :lower() string method instead — it is also Unicode-aware and behaves differently from string.lower in edge cases (for example, the German ß, which lowercases to itself in some systems and to ss in others).

Common Mistakes

The most frequent bug is forgetting that string.lower returns a new string. The original is unchanged, so the call has no effect unless the result is captured. Lua also does not auto-coerce numbers — string.lower(42) raises a type error instead of returning "42". If the input type is uncertain, coerce with tostring first.

For non-ASCII text, the function operates on bytes, not code points, so Unicode folding requires the utf8 library in Lua 5.3+. The result is also not stable across hosts with different C locales; pin the locale on the C side if the code ships to multiple machines.

See Also