Variables and Types in Ruby

· 8 min read · Updated March 26, 2026 · beginner
ruby variables types objects

Everything in Ruby is an object. Before you can work effectively with the language, you need to understand how Ruby organizes values — how you store them, distinguish between different kinds of data, and check what you are dealing with at runtime. This tutorial covers Ruby’s variable system and its type hierarchy from the ground up.

Ruby’s Variable Types

Ruby gives you five distinct variable scopes, each denoted by a prefix character. Using the right scope for the right job keeps your code predictable and easy to test.

Local Variables

A local variable begins with a lowercase letter or an underscore. It is scoped to the block, method, or module where it is defined — it disappears when execution leaves that scope.

name = "Alice"
_age = 30

def greet
  message = "Hello"
  puts message
end

greet  # => Hello
# puts message  # NameError: undefined local variable or method `message'

If you reference a local variable before assigning it, Ruby raises a NameError. This protects you from typos that would otherwise create silent nil values.

Instance Variables

An instance variable begins with @. It belongs to a single object instance — every object gets its own copy. Accessing an uninitialized instance variable returns nil instead of raising an error, which is useful during object construction.

class Person
  def initialize(name)
    @name = name
  end

  def greet
    "Hi, I'm #{@name}"
  end
end

alice = Person.new("Alice")
alice.greet  # => "Hi, I'm Alice"

Instance variables are private to the object. You cannot read @name from outside the instance without a getter method. Ruby’s attr_accessor, attr_reader, and attr_writer macros generate these for you.

Class Variables

A class variable begins with @@. It is shared across every instance of a class and its subclasses. Accessing an uninitialized class variable raises an error rather than returning nil, so you must always initialize before use.

class Counter
  @@count = 0

  def initialize
    @@count += 1
  end

  def self.total
    @@count
  end
end

Counter.new
Counter.new
Counter.new
Counter.total  # => 3

Class variables are shared inheritance-wide, which can produce surprising results when subclasses override them. Instance variables are the better default choice in most cases.

Global Variables

A global variable begins with $. It is accessible from anywhere in your program. Ruby predefines a number of globals such as $LOAD_PATH, $$ (the current process ID), and $SAFE.

$app_name = "MyRubyApp"
$DEBUG = false

def log(msg)
  puts "[#{$app_name}] #{msg}" if $DEBUG
end

Global variables make code difficult to test and reason about because any part of your program can modify them. Reserve $var for genuinely global state and prefer class or instance variables for most use cases.

Constants

A constant begins with an uppercase letter. Ruby conventions use SCREAMING_SNAKE_CASE. Constants are scoped to the class or module where they are defined.

MAX_RETRIES = 5
PI = 3.14159

class MathHelper
  DEFAULT_PRECISION = 4
end

Reassigning a constant produces a warning — Ruby lets you do it, but flags the operation:

PI = 3.14  # warning: already initialized constant PI

One critical caveat: the object a constant points to may still be mutable. Assigning a new value to DEFAULT_OPTIONS[:debug] does not warn, even though it changes the hash contents:

DEFAULT_OPTIONS = { debug: false }
DEFAULT_OPTIONS[:debug] = true  # no warning — the object is mutable

Ruby’s Type Hierarchy

Every value in Ruby is an object. There are no primitives, no boxing, and no value types. Even true, false, and nil are full objects with methods.

Object

Object is the root class for most Ruby objects. Every object inherits from Object, which inherits from BasicObject.

obj = Object.new
obj.class  # => Object

Integer and Float

Ruby has a single Integer class (older Ruby versions distinguished Fixnum and Bignum, but these are now unified). Float represents IEEE 754 double-precision numbers.

42.class      # => Integer
3.14.class    # => Float
42.odd?       # => true
3.14.round    # => 3.0

Integer division truncates toward zero in Ruby 3.x. Use float operands or .to_f when you need decimal results:

5 / 2     # => 2
5 / 2.0   # => 2.5
5.to_f / 2  # => 2.5

String

String holds a mutable sequence of bytes. Ruby distinguishes two quote styles:

  • Single-quoted — minimal escaping; \n is a literal backslash followed by n.
  • Double-quoted — interpolation and escape sequences are processed.
name = "Alice"
greeting = "Hello, #{name}!"  # => "Hello, Alice!"

'hello \n world'  # => "hello \\n world"
"hello \n world"  # => "hello \n world" (newline)

Symbol

A Symbol is an immutable, interned identifier. Every occurrence of :active in your program refers to the same object in memory. This makes symbols efficient as hash keys and as identifiers for method lookups.

:a.object_id == :a.object_id  # => true
"a".object_id == "a".object_id  # => false (different objects)

status = :active
status.class  # => Symbol

Array

An Array is an ordered, integer-indexed collection that grows dynamically.

fruits = ["apple", "banana", "cherry"]
fruits[0]         # => "apple"
fruits[-1]        # => "cherry"
fruits << "date"  # => ["apple", "banana", "cherry", "date"]

Hash

A Hash stores key-value pairs. Keys are compared by equality. A hash literal with key: syntax creates symbol keys by default in modern Ruby.

config = { "host" => "localhost", port: 3000 }
config["host"]  # => "localhost"
config[:port]   # => 3000
config.keys     # => ["host", :port]

String keys and symbol keys are distinct entries in the same hash. Converting between them with .to_sym or .to_s is required when mixing styles.

Boolean

Ruby has two boolean objects: true (TrueClass) and false (FalseClass). Only false and nil are falsy — every other value is truthy, including 0, "", and [].

true.class   # => TrueClass
false.class  # => FalseClass
!!0          # => true (double negation forces boolean)

NilClass

nil represents the absence of a value. It is a proper object of class NilClass.

result = nil
result.class  # => NilClass
result.nil?  # => true

Checking Types at Runtime

Ruby’s approach to types is dynamic: you check capabilities rather than types wherever possible. The language provides several methods for the cases where you do need to inspect an object’s class.

.is_a? and .kind_of?

Both methods check whether an object is an instance of a given class or any of its ancestors. They behave identically:

"hello".is_a?(String)      # => true
"hello".is_a?(Object)      # => true (String < Object)
[1, 2].is_a?(Array)        # => true
[1, 2].kind_of?(Object)    # => true

For strict class matching with no ancestor check, use .instance_of?:

42.instance_of?(Integer)    # => true
42.instance_of?(Numeric)    # => false (Integer is a subclass of Numeric)
42.is_a?(Numeric)            # => true

.respond_to?

This checks whether an object can handle a given method. It accepts an optional second argument to include private methods in the check:

obj = "hello"
obj.respond_to?(:upcase)    # => true
obj.respond_to?(:to_h)      # => false
obj.respond_to?(:puts, true)  # => true (String is not private, Kernel is)

.class

Returns the object’s exact class:

42.class    # => Integer
[].class    # => Array

Duck Typing

Duck typing inverts the traditional question. Instead of asking “what is this object?”, you ask “what can this object do?”. If an object implements the methods you need, it works — regardless of its class.

The name comes from the saying: “if it walks like a duck and quacks like a duck, it’s a duck.”

def print_all(collection)
  collection.each { |item| puts item }
end

print_all(["a", "b", "c"])     # works — Array has #each
print_all({ x: 1, y: 2 })      # works — Hash also has #each

Ruby’s standard collection types all implement each, so this method accepts any of them without changes. Duck typing shines when you write flexible APIs that work with any compatible object.

You can check capabilities explicitly before calling:

def double_if_numeric(value)
  if value.respond_to?(:*)
    value * 2
  else
    value
  end
end

double_if_numeric(5)   # => 10
double_if_numeric("x") # => "x"

Converting Between Types

Ruby provides conversion methods on most objects. These methods return new values rather than mutating the originals.

# String conversion
42.to_s             # => "42"
[1, 2, 3].to_s      # => "[1, 2, 3]"
nil.to_s            # => ""

# Integer and float conversion
"42".to_i           # => 42
"3.14".to_i         # => 3 (truncates)
"3.14".to_f         # => 3.14
"hello".to_i        # => 0 (non-numeric string)
"1010".to_i(2)      # => 10 (binary base)
"ff".to_i(16)       # => 255 (hex base)

# Array conversion
(1..5).to_a         # => [1, 2, 3, 4, 5]
{ a: 1, b: 2 }.to_a # => [[:a, 1], [:b, 2]]

# Hash conversion
[[:a, 1], [:b, 2]].to_h  # => { a: 1, b: 2 }

Symbols vs Strings

Symbols and strings look similar but behave differently under the surface:

FeatureSymbolString
MutabilityImmutableMutable
ComparisonIdentity (:a.object_id == :a.object_id)Value ("a" == "a" but different objects)
MemoryInterned — one object per nameAllocated fresh per literal
Best forHash keys, method names, identifiersUser-facing text, serialization
# Symbol as hash key (preferred in modern Ruby)
config = { timeout: 30, retry: 3 }
config[:timeout]   # => 30

# String key
env = { "HOME" => "/home/alice" }
env["HOME"]       # => "/home/alice"

# Converting between them
key = "timeout"
config[key.to_sym]  # => 30
config[key]          # => nil (string key, symbol in hash)

Ruby 3.x defaults to symbol keys in hash literals written with the key: value syntax. Mixing string and symbol keys in the same hash is legal but rarely necessary.

See Also

  • Keywords — Ruby’s language keywords including nil, true, false, and other foundational constructs
  • Core Classes — Reference for Object, Integer, Float, String, Symbol, Array, and Hash
  • Kernel Methods — Top-level methods available in every Ruby scope, including puts, p, and print

Summary

Ruby’s variable system gives you five scopes — local, instance, class, global, and constant — each suited to different problems. Constants use UPPER_CASE, everything else uses snake_case. Global variables should be rare; class and instance variables cover most object-oriented designs.

Ruby’s type system is uniform: everything is an object, and every object carries its class and capabilities at runtime. Type checks use .is_a? or .respond_to?, and duck typing lets you write code that depends on behavior rather than class names. When you need to convert between types, the to_s, to_i, to_f, to_a, and to_h methods are your standard tools.