Basic Data Types
Every value you work with in Ruby — a number, a word, a true/false flag, even the deliberate absence of a value — is an object that belongs to a specific class. Ruby’s basic data types are the building blocks you reach for in nearly every line of code: integers and floats for numbers, strings and symbols for text and identifiers, and true, false, and nil for logic and absence. Understanding exactly how these types behave, especially Ruby’s unusual truthiness rules and the difference between mutable and immutable objects, will save you from some of the most common bugs beginners write.
Overview: How Data Types Work in Ruby
Ruby has no true "primitives" the way C or Java does. Even a literal like 42 is an instance of a class, and you can call methods on it directly: 42.class returns Integer, and 42.even? returns true. This uniformity is one of Ruby’s defining design decisions — because numbers, strings, symbols, true, false, and even nil are all objects descending from Object, they all understand a common set of messages (class, inspect, respond_to?, and so on), and you never need special syntax to treat a number differently from a string when passing it around.
Ruby’s numeric types are Integer and Float. Integers in Ruby have arbitrary precision — they grow as large as memory allows and never silently overflow the way fixed-width integers do in other languages. Floats are IEEE 754 double-precision numbers, which means they are fast but not exact: a value like 0.1 cannot be represented perfectly in binary, so floating point arithmetic can accumulate small rounding errors. This matters for anything involving money or exact comparisons.
Text comes in two flavors: String and Symbol. A String is a mutable, growable sequence of characters — you can change its contents in place with methods like <<, upcase!, or gsub!. A Symbol, written with a leading colon like :status, looks similar but behaves very differently under the hood: symbols are immutable and interned, meaning Ruby keeps exactly one object in memory for every distinct symbol name. Two occurrences of :active anywhere in your program refer to the literal same object, whereas two occurrences of "active" are two separate String objects that merely happen to hold equal content. This is why symbols are the idiomatic choice for hash keys and internal identifiers — comparing them is a fast identity check rather than a character-by-character comparison, and they use less memory when reused often.
Booleans are not a single type in Ruby. true is the sole instance of the class TrueClass, and false is the sole instance of FalseClass. nil, representing "no value at all," is the sole instance of NilClass. Crucially, Ruby defines truthiness very simply: only nil and false are falsy. Every other value — including 0, an empty string "", and an empty array [] — is truthy. Programmers coming from languages like Python, JavaScript, or C, where 0 or "" are falsy, trip over this constantly, so it is worth committing to memory now.
Syntax
Basic data types in Ruby are created with literal syntax — there is no new keyword needed for the everyday ones:
age = 30 # Integer
price = 19.99 # Float
name = "Ruby" # String (double quotes allow interpolation)
tag = 'ruby' # String (single quotes: no interpolation, fewer escapes)
status = :active # Symbol
is_ready = true # TrueClass
is_done = false # FalseClass
result = nil # NilClass
list = [1, 2, 3] # Array
person = { name: "Ada", age: 36 } # Hash
| Type | Example literal | Class | Notes |
|---|---|---|---|
| Integer | 42, -7, 1_000_000 |
Integer | Arbitrary precision, underscores allowed for readability |
| Float | 3.14, -0.5 |
Float | IEEE 754 double; not exact for decimal fractions |
| String | "hi #{name}", 'hi' |
String | Mutable; double quotes interpolate and support escapes |
| Symbol | :name, :in_progress |
Symbol | Immutable, interned; ideal for identifiers and hash keys |
| Boolean | true, false |
TrueClass, FalseClass | Only these two literals belong to these classes |
| Nil | nil |
NilClass | Represents the deliberate absence of a value |
| Array | [1, 2, 3] |
Array | Ordered, integer-indexed collection |
| Hash | { a: 1, b: 2 } |
Hash | Key-value pairs; symbol keys are idiomatic |
Examples
Example 1: Numbers and arithmetic
age = 30
price = 19.99
big_number = 1_000_000
puts age.class
puts price.class
puts big_number
sum = age + price
puts sum
puts sum.class
puts 7 / 2
puts 7.0 / 2
puts 7 % 2
Output:
Integer
Float
1000000
49.989999999999995
Float
3
3.5
1
The underscore in 1_000_000 is purely a readability separator; Ruby strips it and treats the literal as a plain integer. Notice that adding an Integer and a Float promotes the result to Float. Also notice the two divisions: 7 / 2 is integer division and truncates to 3, while 7.0 / 2 forces floating point division and returns 3.5. This distinction trips up nearly every Ruby beginner at least once.
Example 2: Strings vs. symbols
name = "Ruby"
greeting = "Hello, #{name}! You have #{name.length} letters."
puts greeting
status = :active
puts status.class
puts status == :active
puts status.frozen?
symbol_a = :active
puts symbol_a.object_id == status.object_id
string_a = "active"
string_b = "active"
puts string_a.object_id == string_b.object_id
puts string_a == string_b
Output:
Hello, Ruby! You have 4 letters.
Symbol
true
true
true
false
true
String interpolation with #{...} only works inside double-quoted strings, and it calls to_s on whatever expression is inside the braces. The symbol comparisons show the key difference from strings: every :active literal in the program is the exact same object in memory (their object_ids match, and symbols are always frozen), while two separately written "active" string literals are distinct objects with different identities even though they are equal in content.
Example 3: Truthiness, nil, and keyword arguments
def describe_value(value)
if value
"#{value.inspect} is truthy"
else
"#{value.inspect} is falsy"
end
end
[0, "", [], nil, false, "hello"].each do |v|
puts describe_value(v)
end
def find_user(id:, users:)
users.find { |u| u[:id] == id }
end
people = [{ id: 1, name: "Ada" }, { id: 2, name: "Grace" }]
result = find_user(id: 3, users: people)
puts result.nil?
puts result.inspect
Output:
0 is truthy
"" is truthy
[] is truthy
nil is falsy
false is falsy
"hello" is truthy
true
nil
Even though 0, the empty string, and the empty array feel like "nothing" in other languages, Ruby treats all three as truthy — only nil and false flip an if to its else branch. The second half of the example uses required keyword arguments (id: and users:), which make call sites self-documenting, and shows that Array#find returns nil — not an error — when nothing matches, which is exactly why checking for nil defensively matters.
How It Works Step by Step
Walking through Example 3’s first loop: Ruby first builds the six-element array literal, allocating one Array object. each then iterates left to right, and for every element it invokes describe_value(v). Inside that method, if value asks Ruby’s interpreter to check the object against the truthy/falsy rule described above — this is not a method call on value, it’s a built-in check baked into the language itself. Whichever branch runs produces a string as the method’s return value, because in Ruby a method (and an if expression) always evaluates to the value of the last expression executed — there is no need for an explicit return. That string then flows back to puts, which calls to_s on it (already a string, so a no-op) and writes it followed by a newline.
The keyword-argument call works similarly but at the call boundary: find_user(id: 3, users: people) builds an implicit hash of named arguments, and Ruby matches each key to the corresponding required keyword parameter in the method definition, raising an ArgumentError if one is missing. Inside, users.find { |u| u[:id] == id } passes each hash in the array to the block in turn, stopping and returning the first element for which the block returns a truthy value — and returning nil, not raising an error, if it exhausts the array without a match.
Common Mistakes
Mistake 1: Checking for nil with == false
Because nil and false are both falsy, it’s tempting to treat them as interchangeable — but they are different objects that are not == to each other. Testing a possibly-missing value against false silently misses the nil case:
def stock_count(item)
nil
end
count = stock_count("widget")
if count == false
puts "No stock info available"
else
puts "Count is #{count}"
end
Output:
Count is
The intent was clearly to print a "no info" message, but since nil == false is false, execution falls into the else branch and interpolates an empty string. Use the value’s own truthiness (or nil?) instead of comparing to a specific falsy literal:
def stock_count(item)
nil
end
count = stock_count("widget")
if count
puts "Count is #{count}"
else
puts "No stock info available"
end
Output:
No stock info available
Mistake 2: Expecting integer division to produce a fraction
scores = [10, 3]
average = (scores[0] + scores[1]) / 2
puts average
Output:
6
Both operands are integers, so / performs integer division and truncates toward zero — 13 / 2 becomes 6, silently discarding the remainder. Force at least one operand to a Float to get a fractional result:
scores = [10, 3]
average = (scores[0] + scores[1]) / 2.0
puts average
Output:
6.5
Mistake 3: Mutating a shared reference by accident
original = "hello"
copy = original
copy << " world"
puts original
Output:
hello world
Assignment in Ruby copies the reference, not the object. copy = original makes both variables point to the exact same String object, so mutating copy in place with the bang-like << also changes what original sees. Use dup (or clone) to get an independent copy when you don't want that:
original = "hello"
copy = original.dup
copy << " world"
puts original
puts copy
Output:
hello
hello world
Best Practices
- Use symbols (
:status) for hash keys and internal identifiers; reserve strings for text that a user will actually see or that comes from outside your program. - Never test for a missing value with
== falseor== nil; useif value,unless value, or the explicitvalue.nil?. - Remember that
0,"", and[]are truthy in Ruby — don't port truthiness assumptions from other languages. - Call
.dupor.clonewhen you need an independent copy of a mutable object like aString,Array, orHashinstead of a second reference to the same one. - Use
.to_for a.0literal deliberately whenever a division needs a fractional result. - Prefer keyword arguments (
def find_user(id:, users:)) over long positional argument lists so call sites stay self-documenting. - Remember that bang methods like
sort!returnnilwhen nothing changed — don't chain another call directly onto a bang method's result.
Practice Exercises
- Write a method
average(total:, count:)that returns the average as aFloat, then call it withtotal: 10andcount: 4. Expected output:2.5. - Given the array
[nil, false, 0, "", "ruby"], write code that counts how many elements are truthy and how many are falsy. Expected result: 3 truthy, 2 falsy. - Create two string variables with the same content (e.g. both set to
"ruby") and two symbol variables with the same name (e.g. both set to:ruby). Print whether each pair'sobject_ids match, and explain in a comment why the results differ.
Summary
- Ruby has no primitives — every value, including numbers and
nil, is an object with a class you can inspect with.class. - Only
nilandfalseare falsy;0,"", and[]are all truthy. - Integer division truncates toward zero; use a
Floatoperand when you need a fractional result. - Symbols are immutable and interned (one object per name), making them ideal for hash keys and identifiers; strings are mutable and better suited to user-facing text.
- Assignment copies references for mutable objects, so two variables can point at, and mutate, the same underlying object — use
dupfor an independent copy. trueandfalselive in their own singleton classes,TrueClassandFalseClass, andnillives inNilClass.
