rubyguides

Ruby Variables and Types: A Complete Guide to Scopes and Type Checking

Everything in Ruby is an object, and understanding Ruby variables and types is the foundation for writing clear, predictable code. 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 and make bugs harder to track down.

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 automatically.

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. Because all subclasses see the same @@ variable, changing it in a child class affects the parent and siblings too. 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 the interpreter prints a message to stderr, which means your tests and CI will notice the issue:

Class variables are scoped to a class hierarchy, but global variables have no scope boundary at all. The $ prefix signals that a variable is accessible from anywhere in the program. Ruby ships with several predefined globals like $LOAD_PATH and $$, and you can define your own. Because any part of your code can read or modify a global, they make programs harder to reason about and test. Reserve globals for truly global configuration that rarely changes, such as the application name or a debug flag that you want visible everywhere.

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 a hash key inside a constant does not warn because you are changing the object’s contents, not the constant’s reference:

Constants look like variables but follow a different set of rules. They begin with a capital letter and use SCREAMING_SNAKE_CASE by convention. Unlike globals, constants are scoped to the class or module where they are defined, and Ruby warns you when you try to reassign one. This makes constants the right choice for values that should not change during execution. The MathHelper::DEFAULT_PRECISION pattern shows how constants nest inside classes, keeping configuration close to the code that uses it.

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. Both respond to arithmetic and comparison methods:

The code block above demonstrates a specific Ruby concept. The next code block builds on this by showing a related but distinct aspect of Ruby’s type system or variable handling. Understanding the relationship between these two blocks is key to seeing how Ruby’s features connect together in practice. Reading them in sequence helps build a mental model where each concept reinforces the next, rather than memorizing isolated facts about the language.

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. The division behavior catches newcomers off guard, so it is worth checking whenever a calculation produces an integer where you expected a float:

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.

Strings and symbols both represent textual values, but Ruby treats them very differently in memory. Every occurrence of :active in your program points to the same object, while every "active" creates a new string. Symbols are immutable and interned, which makes them fast to compare and ideal as hash keys or method identifiers. The trade-off is that symbols are never garbage collected, so creating millions of unique symbols from user input can leak memory. Reserve symbols for fixed identifiers and use strings for values that come from outside your program.

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, since comparing two symbols is a fast pointer check:

: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. Arrays can hold mixed types, support negative indexing, and provide a rich set of enumeration methods:

The code block above demonstrates a specific Ruby concept. The next code block builds on this by showing a related but distinct aspect of Ruby’s type system or variable handling. Understanding the relationship between these two blocks is key to seeing how Ruby’s features connect together in practice. Reading them in sequence helps build a mental model where each concept reinforces the next, rather than memorizing isolated facts about the language.

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. You can mix string and symbol keys in the same hash, though it is rarely necessary:

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.

After covering the individual types, it is useful to know how to check types at runtime. Ruby provides is_a? and kind_of? as synonyms that walk the entire ancestor chain. A string is both a String and an Object, so "hello".is_a?(Object) returns true. For stricter matching, instance_of? checks only the exact class with no ancestor lookup. These methods are most useful in debugging and in metaprogramming scenarios where you need to verify an object’s capabilities before calling methods on it.

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?. It returns true only when the object’s class is exactly the one you specify, ignoring inheritance entirely:

The code block above demonstrates a specific Ruby concept. The next code block builds on this by showing a related but distinct aspect of Ruby’s type system or variable handling. Understanding the relationship between these two blocks is key to seeing how Ruby’s features connect together in practice. Reading them in sequence helps build a mental model where each concept reinforces the next, rather than memorizing isolated facts about the language.

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. Duck typing relies heavily on respond_to? to decide what an object can do:

Duck typing shifts the question from “what type is this?” to “what can this object do?” The respond_to? method checks whether an object handles a given method name, which is more flexible than checking class ancestry. The double_if_numeric method works with integers because they respond to *, but it also works with floats, custom numeric types, or any object that implements multiplication. This is the standard Ruby approach: write code that depends on behaviour, not on class names.

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. This is useful for debugging, but avoid using .class comparisons for control flow. Duck typing with respond_to? is more flexible and idiomatic:

The code block above demonstrates a specific Ruby concept. The next code block builds on this by showing a related but distinct aspect of Ruby’s type system or variable handling. Understanding the relationship between these two blocks is key to seeing how Ruby’s features connect together in practice. Reading them in sequence helps build a mental model where each concept reinforces the next, rather than memorizing isolated facts about the language.

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:

Duck typing keeps your code flexible, but sometimes you genuinely need to convert between types. Ruby’s standard conversion methods (to_s, to_i, to_f, to_a, to_h) appear on nearly every object. The integer conversion "42".to_i parses leading digits and ignores the rest, which means "42 apples".to_i returns 42 without raising an error. The base argument on to_i lets you parse binary, hex, or any radix string. These conversions return new objects rather than mutating the originals, which is consistent with Ruby’s preference for immutability in core methods.

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.

The code block above demonstrates a specific Ruby concept. The next code block builds on this by showing a related but distinct aspect of Ruby’s type system or variable handling. Understanding the relationship between these two blocks is key to seeing how Ruby’s features connect together in practice. Reading them in sequence helps build a mental model where each concept reinforces the next, rather than memorizing isolated facts about the language.

# 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.

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.

Next steps

Variables and types are the vocabulary of Ruby, but they become powerful when combined with control flow. Continue with Ruby Control Flow to learn how conditionals and loops let you make decisions based on the values stored in those variables. You can also explore Ruby Strings for a deeper look at one of the most frequently used types in real Ruby programs.

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