Lesson 2 • Beginner
Variables and Data Types
By the end of this lesson you'll store text, numbers, and true/false values in Lua, know its handful of types, join strings with .., measure them with #, convert between types on purpose, and understand why local should be your default — the bedrock of every Lua script.
What You'll Learn
- Declare variables and tell local apart from global (and why local wins)
- Name Lua's 8 types and use the 5 you'll meet daily
- Understand dynamic typing — the value has a type, the name doesn't
- Join text with the .. operator and avoid the + trap
- Measure length with #, and convert with tostring / tonumber
- Recognise nil and handle 'missing value' safely
print(...). Lua doesn't run in the browser — try each example at onecompiler.com/lua or with the lua command. We'll build everything else here.💡 Real-World Analogy
A global variable is like writing on a shared whiteboard in a busy office — anyone in any room can read it, and anyone can wipe it or scribble over it. A local variable is a sticky note inside your own notebook: only you, in this room, can see it, and it's thrown away when you leave. Most bugs in beginner Lua come from scribbling on the shared whiteboard by accident, so you'll reach for the notebook (local) almost every time.
1. local vs Global Variables
In Lua you make a variable just by assigning to a name: score = 100. But here's the surprise that catches everyone — that variable is global by default. To keep it tidy and safe you put local in front: local score = 100. A global is visible to your whole program and every library; a local only lives inside its current block. Read this worked example, run it, and watch the local vanish outside its block.
-- A variable is a NAMED box that holds one value.
-- In Lua you create one by just assigning to a name:
score = 100 -- this is a GLOBAL variable (avoid this!)
local lives = 3 -- this is a LOCAL variable (almost always what you want)
-- Why does it matter? A global is visible to EVERY part of your program,
-- so a typo or a clash can quietly break code far away. A local only
-- lives inside the block it was declared in, so it stays out of the way.
print(score) -- prints: 100
print(lives) -- prints: 3
-- Locals disappear at the end of their block. 'do ... end' is just a block.
do
local secret = 42 -- only exists between 'do' and 'end'
print(secret) -- prints: 42
end
print(secret) -- prints: nil (secret is gone — it was local to the block)The #1 Lua Gotcha
Because a bare assignment creates a global, a typo silently makes a new variable instead of erroring. Write socre = 100 when you meant score, and Lua happily creates socre — your real value never changes and nothing warns you. Defaulting to local turns this whole class of bug into something you can actually spot.
2. The Eight Types (and Dynamic Typing)
Lua keeps things small: there are exactly 8 types — nil, boolean, number, string, table, function, userdata, and thread. Day to day you'll use the first five. Note that one number type covers both whole numbers and decimals. Lua is dynamically typed: a variable name carries no type at all — only the value inside it does — so the same name can hold a number now and a string a moment later. Use type(x) any time you want to ask what a value is.
-- Lua has 8 types. As a beginner you mostly meet the first five.
-- 'type(x)' tells you the type of any value as a string.
local nothing = nil -- nil = "no value / does not exist"
local ok = true -- boolean = true or false
local age = 25 -- number = ints and decimals (one type)
local pi = 3.14159 -- number too
local name = "Ada" -- string = text, in "double" or 'single' quotes
local scores = {10, 20, 30} -- table = Lua's one container type
print(type(nothing)) -- prints: nil
print(type(ok)) -- prints: boolean
print(type(age)) -- prints: number
print(type(name)) -- prints: string
print(type(scores)) -- prints: table
-- DYNAMIC TYPING: a variable has no fixed type — only the VALUE does.
-- The same name can hold a number now and a string later.
local x = 10
print(type(x)) -- prints: number
x = "now I'm text"
print(type(x)) -- prints: string📊 Lua's 8 Types at a Glance
| Type | Holds | Example |
|---|---|---|
| nil | No value / absence | local x = nil |
| boolean | true or false | local ok = true |
| number | Ints and decimals | local n = 3.14 |
| string | Text | local s = "hi" |
| table | Lua's only container | local t = {1, 2} |
| function | Reusable behaviour | local f = print |
| userdata | Data from C code | (advanced) |
| thread | A coroutine | (advanced) |
The greyed rows (function, userdata, thread) come later — you'll meet function and table properly in the next lesson.
Your turn. The program below is almost finished — fill in the three blanks marked ___ using the hints, then run it and compare with the expected output.
-- 🎯 YOUR TURN — replace each ___ then run the code.
-- 1) Make a LOCAL string called "player" set to any name
local player = ___ -- 👉 text in "quotes", e.g. "Sam"
-- 2) Make a LOCAL number called "level" set to a whole number
local level = ___ -- 👉 a number, no quotes, e.g. 5
-- 3) Build a message by concatenating with .. (two dots)
-- Hint: "text" .. variable .. "more text"
local message = "Player " .. ___ .. " is on level " .. ___ -- 👉 use player, then level
print(message)
-- ✅ Expected output:
-- Player Sam is on level 53. Strings: Joining, Length, and Converting
To glue text together you use the concatenation operator .. (two dots) — not +, which is only for number maths. To find how long a string is, put the length operator # in front of it: #"Lua" is 3. When you need to move between text and numbers deliberately, tostring(value) turns anything into text and tonumber(text) turns text into a number (or gives nil if it isn't one). Numbers used inside .. convert to text for you automatically.
-- Join (concatenate) strings with two dots: ..
local first = "Grace"
local last = "Hopper"
local full = first .. " " .. last
print(full) -- prints: Grace Hopper
-- '..' joins; it is NOT '+'. Using '+' on text is an error (see below).
-- Lua will auto-convert a NUMBER to text inside '..':
local lvl = 7
print("Level " .. lvl) -- prints: Level 7
-- # is the LENGTH operator. On a string it counts the bytes (characters).
print(#full) -- prints: 12 (G-r-a-c-e-space-H-o-p-p-e-r)
print(#"Lua") -- prints: 3
-- Convert ON PURPOSE with tostring() and tonumber():
local n = 42
print(tostring(n) .. "!") -- prints: 42! (number -> string)
print(tonumber("3.5") + 0.5) -- prints: 4.0 (string -> number)
print(tonumber("hello")) -- prints: nil (not a number -> nil)Now you try. Text typed by a user always arrives as a string, so before doing maths you convert it. Fill in the two blanks:
-- 🎯 YOUR TURN — input from a user always arrives as TEXT.
local quantityText = "3" -- this is a STRING, not a number
local word = "dragon"
-- 1) Turn quantityText into a real number with tonumber(...)
local quantity = ___ -- 👉 tonumber(quantityText)
-- 2) Get the length of "word" with the # operator
local letters = ___ -- 👉 #word
-- 3) Concatenate: numbers auto-convert, so just join with ..
print("You have " .. quantity .. " items")
print("The word has " .. letters .. " letters")
-- ✅ Expected output:
-- You have 3 items
-- The word has 6 lettersPro Tips
- 💡 Default to
local. Put it on every variable unless you genuinely need a global. Your future self will thank you. - 💡
..joins,+adds. If you mean text, use..; if you see an arithmetic error on a string, that's the cause. - 💡 Convert on purpose. Reach for
tonumberthe moment a value comes from user input, a file, or the network — it's text until you say otherwise. - 💡 Multi-line strings use double square brackets:
[[ ... ]]. They keep newlines and whitespace exactly, great for templates.
Common Errors (and the fix)
- Forgetting
localmakes a global:health = 50quietly pollutes the global namespace, and a typo creates a stray global. Fix: writelocal health = 50. - "attempt to perform arithmetic on a string value": you joined text with
+, e.g."Hi " + name. Fix: use..→"Hi " .. name. - "attempt to compare number with string": Lua won't compare across types, e.g.
10 < "20". Fix: convert first —10 < tonumber("20"). - "attempt to perform arithmetic on a nil value": you used a variable that was never set (or a
tonumberthat returnednil) in maths, likenil + 1. Fix: check the value exists before doing arithmetic.
📋 Quick Reference
| Task | Code | Result |
|---|---|---|
| Local variable | local x = 10 | scoped to block |
| Check a type | type("hi") | "string" |
| Join text | "a" .. "b" | "ab" |
| String length | #"hello" | 5 |
| Text → number | tonumber("42") | 42 |
| Number → text | tostring(42) | "42" |
| No value | local x = nil | nil |
Frequently Asked Questions
Q: Why should I almost always use local instead of global?
A local variable only exists inside the block where you declare it, so it can't accidentally clash with code elsewhere, and the Lua compiler can access it faster. Globals live in one shared table that the whole program (and every library) can see and overwrite, so a single typo can silently break distant code. The rule of thumb: write 'local' in front of every variable unless you have a specific reason not to.
Q: What happens if I forget the word local?
Lua does not error — it creates a GLOBAL variable instead. That is why typos are dangerous: writing 'socre = 100' instead of 'score = 100' makes a brand-new global called socre, and your real score variable is never updated. Nothing warns you. Putting 'local' on declarations makes such mistakes far easier to catch.
Q: How many data types does Lua have?
Eight: nil, boolean, number, string, table, function, userdata, and thread. Beginners mainly use the first five. 'function' values let you store and pass behaviour; 'userdata' wraps data from C libraries; 'thread' is for coroutines. You can always check any value's type at runtime with type(value).
Q: Why do I join strings with .. and not +?
In Lua the .. operator means 'concatenate' (stick text together), while + is reserved strictly for number arithmetic. Writing "Hi " + name throws an 'attempt to perform arithmetic on a string value' error. Use "Hi " .. name instead. Numbers used inside .. are converted to text automatically.
Q: What is nil and when do I see it?
nil means 'no value here'. An unset variable, a missing table key, and a tonumber() call on non-numeric text all give back nil. Reading a variable that was never assigned returns nil rather than crashing — but doing arithmetic on nil (like nil + 1) is an error, so check for nil before using a value you are unsure about.
Mini-Challenge: Character Sheet
No blanks this time — just a brief and an outline to keep you on track. Build it, run it, and check your output against the example in the comments. This is the kind of tiny program real games and tools are stitched together from.
-- 🎯 MINI-CHALLENGE: Character sheet
-- 1. Create LOCAL variables: name (string), class (string),
-- hp (number), and alive (boolean set to true).
-- 2. Print a 3-line sheet using .. to join text and values:
-- Name: <name> the <class>
-- HP: <hp>
-- Alive: <alive>
-- 3. BONUS: print the length of the name with #name on its own line.
--
-- ✅ Example output (name="Mara", class="Mage", hp=30):
-- Name: Mara the Mage
-- HP: 30
-- Alive: true
-- Name length: 4
-- your code here🎉 Lesson Complete!
- ✅ Variables are global by default — write
localto keep them scoped and safe - ✅ A forgotten
local(or a typo) silently creates a global - ✅ Lua has 8 types; you'll mostly use
nil,boolean,number,string,table - ✅ Lua is dynamically typed — the value has a type, the variable name doesn't
- ✅ Join text with
.., measure it with#, convert withtostring/tonumber - ✅
nilmeans "no value"; arithmetic onnilerrors, so check first - ✅ Next lesson: Functions and Tables — package behaviour and store structured data
Sign up for free to track which lessons you've completed and get learning reminders.