Basic Data Types

Published by Programming Line in Ruby. Last updated .

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 == false or == nil; use if value, unless value, or the explicit value.nil?.
  • Remember that 0, "", and [] are truthy in Ruby — don't port truthiness assumptions from other languages.
  • Call .dup or .clone when you need an independent copy of a mutable object like a String, Array, or Hash instead of a second reference to the same one.
  • Use .to_f or a .0 literal 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! return nil when nothing changed — don't chain another call directly onto a bang method's result.

Practice Exercises

  1. Write a method average(total:, count:) that returns the average as a Float, then call it with total: 10 and count: 4. Expected output: 2.5.
  2. 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.
  3. 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's object_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 nil and false are falsy; 0, "", and [] are all truthy.
  • Integer division truncates toward zero; use a Float operand 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 dup for an independent copy.
  • true and false live in their own singleton classes, TrueClass and FalseClass, and nil lives in NilClass.