Variables and Types
Intuition
Section titled “Intuition”Ruby’s type system is dynamic and duck-typed, meaning variables have no fixed types and objects respond to messages based on their capabilities rather than their class hierarchy. This flexibility enables rapid prototyping but shifts type checking from compile time to runtime. Constants, instance variables, class variables, and global variables each have distinct scoping rules that determine where data is accessible. Ruby’s object model treats everything as an object, enabling a uniform interface for all values.
Variables in Ruby
Section titled “Variables in Ruby”Ruby has several types of variables, each with distinct scope and purpose. Variables do not need explicit type declarations — Ruby is dynamically typed, meaning the interpreter determines types at runtime.
Variable Types and Scope
Section titled “Variable Types and Scope”## Local variable -- lowercase or underscorename = "Alice"count = 42_private = "convention for internal use"
## Instance variable -- prefixed with @# Belongs to a specific object instance@name = "Bob"@items = []
# Class variable -- prefixed with @@# Shared across all instances of a class and its subclasses@@count = 0
# Global variable -- prefixed with $# Accessible everywhere in the program$global_var = "global"
# Constant -- starts with uppercase letter# Should not be reassigned (Ruby warns but allows it)PI = 3.14159MAX_SIZE = 100MyModule = Module.new
# Pseudovariables -- cannot be assignedself # current receivernil # nothing / absence of valuetrue # boolean truefalse # boolean false__FILE__ # current file path__LINE__ # current line number__ENCODING__ # current file encodingVariable Scoping Rules
Section titled “Variable Scoping Rules”x = "outer"
1.times do x = "inner" # modifies the outer x y = "local" # local to this blockend
puts x # => "inner" -- blocks share the enclosing scope# puts y # => NameError: undefined local variable or method "y'
# Loop variables leak out of blocks (Ruby < 3.0 behaviour preserved for compatibility)result = [1, 2, 3].each do |item| # item is accessible hereend# item => 3 in Ruby < 3.0, nil in Ruby 3.0+
# Methods create a new scopedef example z = "method local"end
# z is not accessible here -- NameErrorNaming Conventions
Section titled “Naming Conventions”Ruby follows strict naming conventions that communicate intent and scope:
| Convention | Usage | Example |
|---|---|---|
snake_case | Methods, local variables, instance variables | user_name, calculate_total |
UPPER_SNAKE_CASE | Constants | MAX_RETRIES, API_VERSION |
CamelCase | Class and module names | UserAccount, HttpClient |
@snake_case | Instance variables | @first_name, @is_active |
@@snake_case | Class variables | @@instance_count |
$snake_case | Global variables (avoid when possible) | $stdin, $LOAD_PATH |
? suffix | Predicate methods (return boolean) | empty?, include?, valid? |
! suffix | Dangerous/mutating methods | sort!, save!, chomp! |
= suffix | Setter methods | name=, email= |
# Good naming examplesdef is_valid?(input) input.length > 0 && input.match?(/\A[a-z]+\z/)end
def calculate_total(order) order.items.sum(&:price) * (1 + order.tax_rate)end
# Constants are uppercaseMAX_CONNECTIONS = 100DEFAULT_TIMEOUT = 30
# Classes are CamelCaseclass UserAccount # Instance variables def initialize(username) @username = username @login_count = 0 end
# Predicate method def active? @login_count > 0 end
# Dangerous method def reset! @login_count = 0 self end
# Setter def username=(new_name) @username = new_name endendData Types
Section titled “Data Types”Ruby is a purely object-oriented language. Every value is an object with its own class and methods.
Numeric Types
Section titled “Numeric Types”# Integers -- arbitrary precision (no overflow)42.class # => Integer1_000_000.class # => Integer (underscores are ignored)0b1010 # binary => 100o755 # octal => 4930xFF # hexadecimal => 255-42.class # => Integer (negative integers are also Integer)
# Integer operations10 + 3 # => 1310 - 3 # => 710 * 3 # => 3010 / 3 # => 3 (integer division for two integers)10.0 / 3 # => 3.3333... (float division when either operand is Float)10 % 3 # => 1 (modulo)10 ** 3 # => 1000 (exponentiation)
# Integer predicates42.even? # => true42.odd? # => false42.zero? # => false1.positive? # => true-1.negative? # => true
# Bitwise operations0b1100 & 0b1010 # => 8 (AND)0b1100 | 0b1010 # => 14 (OR)0b1100 ^ 0b1010 # => 6 (XOR)~0b1100 # => -13 (NOT)0b1100 >> 1 # => 6 (right shift)0b1100 << 1 # => 24 (left shift)
# Floats -- IEEE 754 double precision3.14.class # => Float2.0e10 # => 20000000000.0 (scientific notation)1.0 / 3.0 # => 0.3333333333333333Float::INFINITY # => InfinityFloat::NAN # => NaNFloat::EPSILON # => 2.220446049250313e-16
# Float comparison caveats0.1 + 0.2 == 0.3 # => false (floating-point precision)(0.1 + 0.2).round(10) == 0.3 # => true (use rounding for comparison)
# Rational numbers -- exact fractions1/3r # => (1/3)(1/3r) * 3 # => (1/1) -- exactRational(22, 7) # => (22/7)
# Complex numbersComplex(3, 4) # => (3+4i)(3 + 4i).abs # => 5.0(3 + 4i).conjugate # => (3-4i)String
Section titled “String”Strings are mutable sequences of characters (in Ruby, each character is a String of length 1):
# String creation"hello" # double-quoted -- supports interpolation and escapes'hello' # single-quoted -- literal, no interpolation%(hello world) # percent literal -- like double-quoted%Q(hello #{name}) # same as double-quoted%q(hello) # same as single-quoted
# String interpolation (double-quoted only)name = "Ruby"version = 3.3"#{name} #{version}" # => "Ruby 3.3""Result: #{42 + 8}" # => "Result: 50""#{'upcase'.upcase}" # => "UPCASE" (any expression)
# Multiline stringslong_text = <<~HEREDOC This is a heredoc string. The ~ operator strips leading whitespace. Variables interpolate: #{name}HEREDOC
# Common string operations"hello".length # => 5"hello".reverse # => "olleh""hello".upcase # => "HELLO""hello".downcase # => "hello""hello".capitalize # => "Hello""Hello World".include?("World") # => true"Hello World".index("World") # => 6"hello" == "hello" # => true"hello".equal?("hello") # => false (different objects)"hello".eql?("hello") # => true (same content and type)
# Substring extraction"hello"[0] # => "h""hello"[0, 3] # => "hel""hello"[1..3] # => "ell""hello"[-1] # => "o""hello"[-3..-1] # => "llo"
# String replacement"hello".sub("l", "r") # => "herlo" (first occurrence)"hello".gsub("l", "r") # => "herro" (all occurrences)"hello".sub(/l/) { "r" } # => "herlo" (block form)
# Splitting and joining"a,b,c".split(",") # => ["a", "b", "c"]"a,b,c".split(",", 2) # => ["a", "b,c"] (limit splits)["a", "b", "c"].join(",") # => "a,b,c"["a", "b", "c"].join # => "abc"["a", "b", "c"].join("-") # => "a-b-c"
# Stripping whitespace" hello ".strip # => "hello"" hello ".lstrip # => "hello "" hello ".rstrip # => " hello"" hello ".chomp # => " hello" (removes trailing newline)
# Padding"hello".ljust(10, "-") # => "hello-----""hello".rjust(10, "-") # => "-----hello""hello".center(11, "-") # => "---hello---"
# String formattingsprintf("%05d", 42) # => "00042""%.2f" % 3.14159 # => "3.14""%-20s" % "left" # => "left ""%s is %d years old" % ["Alice", 30] # => "Alice is 30 years old"
# Encoding"hello".encoding # => #<Encoding:UTF-8>"hello".bytesize # => 5"hello".force_encoding("ASCII")Symbol
Section titled “Symbol”Symbols are immutable, interned identifiers. Two symbols with the same name are the same object:
:hello.class # => Symbol:hello.object_id == :hello.object_id # => true (same object)"hello".object_id == "hello".object_id # => false (different objects)
# Symbols are commonly used as hash keys and identifiersperson = { name: "Alice", age: 30 }# equivalent to:person = { :name => "Alice", :age => 30 }
# Symbol conversion"hello".to_sym # => :hello:i_am.to_s # => "i_am":i_am.intern # => :i_am (same as to_sym)
# When to use Symbol vs String# Symbol: fixed identifiers, hash keys, method names, enum-like values# String: mutable text, user input, external data
# Symbol performance advantage in hashes# Symbol lookup is O(1) because symbols are interned{ name: "Alice" } # preferred{ "name" => "Alice" } # creates new string key each time (pre-Ruby 2.2)
# Ruby 2.2+ optimises frozen string keys, but symbol keys remain conventionalBoolean: true and false
Section titled “Boolean: true and false”true.class # => TrueClassfalse.class # => FalseClass
# Truthiness in Ruby# Only nil and false are falsy; everything else is truthy (including 0, "", [])if 0 "0 is truthy"end# => "0 is truthy"
if "" "empty string is truthy"end# => "empty string is truthy"
if nil "nil is falsy"else "nil is falsy"end
if false "false is falsy"else "false is falsy"endnil.class # => NilClassnil.nil? # => truenil.to_s # => ""nil.to_i # => 0nil.to_f # => 0.0nil.to_a # => []nil&.length # => nil (safe navigation)
# nil is a singleton -- there is only one nil objectnil.object_id == nil.object_id # => true
# Common pattern: default value with ||name = nildisplay_name = name || "Anonymous" # => "Anonymous"
# Be careful: || does not distinguish nil from falseactive = falsestatus = active || "inactive" # => "inactive" (false is truthy check fails)Arrays are ordered, integer-indexed collections of any type:
# Creation[1, 2, 3] # literalArray.new(3, "x") # => ["x", "x", "x"]Array.new(3) { |i| i * 2 } # => [0, 2, 4]%w[apple banana cherry] # => ["apple", "banana", "cherry"]%i[apple banana cherry] # => [:apple, :banana, :cherry]Array(1..5) # => [1, 2, 3, 4, 5]Array.new([1, 2, 3]) # => [1, 2, 3]
# Accessarr = [10, 20, 30, 40, 50]arr[0] # => 10arr[-1] # => 50arr[1, 3] # => [20, 30, 40]arr[1..3] # => [20, 30, 40]arr.fetch(99, "default") # => "default"arr.first # => 10arr.last # => 50
# Modificationarr.push(60) # => [10, 20, 30, 40, 50, 60]arr << 70 # => [10, 20, 30, 40, 50, 60, 70]arr.pop # => 70arr.shift # => 10arr.unshift(5) # => [5, 20, 30, 40, 50, 60]arr.insert(2, 25) # => [5, 20, 25, 30, 40, 50, 60]arr.delete(30) # => 30 (removes and returns)arr.delete_at(1) # => 20
# Higher-order methods[1, 2, 3, 4, 5].map { |n| n * 2 } # => [2, 4, 6, 8, 10][1, 2, 3, 4, 5].select { |n| n > 3 } # => [4, 5][1, 2, 3, 4, 5].reject { |n| n > 3 } # => [1, 2, 3][1, 2, 3, 4, 5].find { |n| n > 3 } # => 4[1, 2, 3, 4, 5].find_index { |n| n > 3 } # => 3[1, 2, 3, 4, 5].count { |n| n > 3 } # => 2[1, 2, 3, 4, 5].reduce(0) { |sum, n| sum + n } # => 15[1, 2, 3, 4, 5].sort # => [1, 2, 3, 4, 5][3, 1, 4, 1, 5].uniq # => [1, 3, 4, 5][1, 2, 3].flatten # => [1, 2, 3][[1, 2], [3, 4]].transpose # => [[1, 3], [2, 4]]
# Combination[1, 2].product([3, 4]) # => [[1,3],[1,4],[2,3],[2,4]][1, 2].zip([3, 4], [5, 6]) # => [[1,3,5],[2,4,6]][1, 2, 3, 4].each_slice(2).to_a # => [[1,2],[3,4]][1, 2, 3, 4].each_cons(2).to_a # => [[1,2],[2,3],[3,4]]
# Array predicates[].empty? # => true[1, 2, 3].include?(2) # => true[1, 2, 3].any? { |n| n > 2 } # => true[1, 2, 3].all? { |n| n > 0 } # => true[1, 2, 3].none? { |n| n > 5 } # => true[1, 2, 3].one? { |n| n > 2 } # => true
# Array arithmetic[1, 2] + [3, 4] # => [1, 2, 3, 4][1, 2, 3] - [2] # => [1, 3][1, 2] * 3 # => [1, 2, 1, 2, 1, 2][1, 2] & [2, 3] # => [2] (intersection)[1, 2] | [2, 3] # => [1, 2, 3] (union)Hashes are key-value collections with O(1) average lookup:
# Creation{ a: 1, b: 2 } # symbol keys{ "name" => "Alice", "age" => 30 } # string keysHash.new(0) # default value for missing keysHash.new { |h, k| h[k] = [] } # auto-initialising default
# Accessh = { name: "Alice", age: 30 }h[:name] # => "Alice"h[:missing] # => nilh.fetch(:missing, "N/A") # => "N/A"h.key?(:name) # => trueh.value?(30) # => trueh.keys # => [:name, :age]h.values # => ["Alice", 30]
# Modificationh[:email] = "alice@example.com" # add/updateh.delete(:age) # removeh.transform_keys(&:to_s) # => {"name"=>"Alice", "email"=>"..."}h.transform_values(&:to_s) # => {:name=>"Alice", ...}h.merge({ city: "London" }) # => new hash with merged entriesh.merge!({ city: "London" }) # modify in place
# Iterationh.each { |key, value| puts "#{key}: #{value}" }h.each_key { |key| puts key }h.each_value { |value| puts value }
# Hash predicatesh.empty? # => falseh.has_key?(:name) # => trueh.has_value?("Alice") # => true{ a: 1, b: 2 }.any? { |_k, v| v > 1 } # => true
# Hash as kwargs (modern Ruby)def configure(host:, port:, timeout: 30) puts "#{host}:#{port} (timeout: #{timeout})"end
configure(**{ host: "localhost", port: 8080 })
# Hash ordering is guaranteed (insertion order) since Ruby 1.9{ a: 1, b: 2, c: 3 }.keys # => [:a, :b, :c] (insertion order)Ranges represent an interval of values:
# Creation1..10 # inclusive range1...10 # exclusive range (excludes 10)('a'..'z') # character range
# Conversion(1..5).to_a # => [1, 2, 3, 4, 5](1...5).to_a # => [1, 2, 3, 4]('a'..'e').to_a # => ["a", "b", "c", "d", "e"]
# Range operations(1..10).include?(5) # => true(1..10).cover?(5.5) # => true (optimised, no iteration)(1..10).min # => 1(1..10).max # => 10(1..10).size # => 10(1..10).begin # => 1(1..10).end # => 10
# Ranges as conditionsscore = 85
case scorewhen 90..100 then "A"when 80...90 then "B"when 70...80 then "C"else "F"end# => "B"
# Ranges in iteration(1..5).each { |i| puts i }3.times { |i| puts i } # 0, 1, 21.upto(5) { |i| puts i } # 1, 2, 3, 4, 55.downto(1) { |i| puts i } # 5, 4, 3, 2, 1
# Ranges for array slicingarr = [0, 1, 2, 3, 4, 5]arr[2..4] # => [2, 3, 4]arr[2...4] # => [2, 3]Duck Typing
Section titled “Duck Typing”Ruby uses duck typing — “If it walks like a duck and quacks like a duck, then it must be a duck.” Objects are classified by what they can do (their methods), not by their class hierarchy:
# Any object that responds to :quack and :waddle works heredef make_it_quack(thing) if thing.respond_to?(:quack) thing.quack else raise ArgumentError, "#{thing} doesn't know how to quack" endend
class Duck def quack; puts "Quack!"; end def waddle; puts "Waddle waddle"; endend
class Person def quack; puts "I'm pretending to be a duck"; endend
make_it_quack(Duck.new) # => "Quack!"make_it_quack(Person.new) # => "I'm pretending to be a duck"
# More practical example: any object with each works as a collectiondef process_all(collection) collection.each do |item| puts item endend
process_all([1, 2, 3]) # Arrayprocess_all({ a: 1, b: 2 }) # Hashprocess_all(1..5) # Rangeprocess_all("hello") # String
# respond_to? for safe method checkingdef safe_length(obj) if obj.respond_to?(:length) obj.length else 0 endend
safe_length("hello") # => 5safe_length([1, 2, 3]) # => 3safe_length(42) # => 0Mutability and Immutability
Section titled “Mutability and Immutability”Most Ruby objects are mutable by default. Strings, arrays, and hashes can be modified in place:
# Strings are mutablename = "hello"name << " world"name.replace("goodbye")name.upcase!puts name # => "GOODBYE"
# Arrays are mutablearr = [1, 2, 3]arr << 4arr[0] = 99arr.clear
# Hashes are mutableh = { a: 1 }h[:b] = 2h.delete(:a)
# Symbols, Integers, Floats, true, false, nil are immutablesym = :hellosym.upcase! # => NoMethodError (Symbol has no mutating methods)freeze
Section titled “freeze”The freeze method prevents further modification of an object:
# Freezing stringsstr = "hello".freezestr << " world" # => FrozenError: can't modify frozen Stringstr.gsub!("l", "r") # => FrozenError
# Frozen object is still usableputs str.length # => 5puts str.upcase # => "HELLO" (returns new string, doesn't modify)
# Check if frozenstr.frozen? # => true
# Freeze with frozen_string_literal pragma# At the top of a file:# frozen_string_literal: true
# All string literals become frozen by defaultgreeting = "hello" # frozengreeting.frozen? # => true
# To create a mutable string:greeting = +"hello" # mutable string literalgreeting = "hello".dup # create a mutable copy
# Freeze other objectsarr = [1, 2, 3].freezearr << 4 # => FrozenError
h = { a: 1 }.freezeh[:b] = 2 # => FrozenError
# Freeze does not deep-freezeouter = ["inner"]outer.freezeouter[0] << " appended" # works! inner array is not frozen
# Deep freeze utilitydef deep_freeze(object) case object when Array object.each { |e| deep_freeze(e) } when Hash object.each { |k, v| deep_freeze(k); deep_freeze(v) } end object.freezeendObject References
Section titled “Object References”Ruby variables hold references to objects, not the objects themselves. Understanding this is critical for avoiding bugs:
# Two variables pointing to the same objecta = "hello"b = ab << " world"puts a # => "hello world" -- both a and b reference the same string
# Object identitya = "hello"b = "hello"a.equal?(b) # => false (different objects)a == b # => true (same content)a.eql?(b) # => true (same content and type)
c = aa.equal?(c) # => true (same object)
# .object_id for identitya = "hello"b = aa.object_id == b.object_id # => true
b = a.dup # shallow copy -- new object, same contenta.object_id == b.object_id # => false
b = a.clone # similar to dup, copies frozen state and singleton methodsa.object_id == b.object_id # => false
# Dup vs cloneoriginal = "hello"original.freeze
duped = original.dupduped.frozen? # => false (dup does not copy frozen state)
cloned = original.clonecloned.frozen? # => true (clone copies frozen state)
# Mutable default argument pitfalldef add_item(items = []) items << "new item" itemsend
add_item # => ["new item"]add_item # => ["new item", "new item"] -- same array reused!add_item # => ["new item", "new item", "new item"]
# Fix: use nil default and create new array insidedef add_item(items = nil) items ||= [] items << "new item" itemsendBasic I/O
Section titled “Basic I/O”Output
Section titled “Output”# puts -- prints with newlineputs "Hello, World!" # => "Hello, World!\n"puts 42 # => "42\n"puts [1, 2, 3] # => "1\n2\n3\n" (each element on its own line)
# print -- prints without newlineprint "Hello, "print "World!" # => "Hello, World!"
# p -- prints with inspect representation (useful for debugging)p "hello" # => "hello" (with quotes)p [1, "two"] # => [1, "two"]
# pp -- pretty print (built-in since Ruby 2.5)pp({ name: "Alice", scores: [85, 92, 78], active: true })
# printf -- formatted outputprintf("Name: %-10s Age: %03d\n", "Alice", 30)# => "Name: Alice Age: 030\n"
# write to $stdout directly$stdout.write("data\n")$stdout.flush
# Logger for structured outputrequire 'logger'log = Logger.new($stdout)log.info("Application started")log.warn("Deprecated feature used")log.error("Connection failed")# gets -- reads a line from stdin (includes newline)input = gets# => "hello\n"input.chomp # => "hello" (removes trailing newline)input.strip # => "hello" (removes leading/trailing whitespace)
# gets with chomp shorthandinput = gets.chomp
# Reading multiple lineslines = []while (line = gets) lines << line.chompend
# Reading from ARGF (files passed as arguments, or stdin)# ruby script.rb file1.txt file2.txtARGF.each_line do |line| puts lineend
# Reading entire inputall_input = gets(nil) # reads all input until EOFdata = $stdin.read
# Command-line argumentsARGV # => Array of command-line argumentsARGV[0] # first argumentARGV.length # number of arguments
# Reading files directlyFile.read("data.txt") # entire file as stringFile.readlines("data.txt") # array of linesFile.foreach("data.txt") { |line| } # iterate lines (memory efficient)File.open("data.txt", "r") do |f| # block form auto-closes f.each_line do |line| puts line endendType Conversion
Section titled “Type Conversion”# String to number"42".to_i # => 42"3.14".to_f # => 3.14"42".to_r # => (42/1)"0xFF".to_i(16) # => 255 (with base)
"abc".to_i # => 0 (returns 0 for non-numeric strings)Integer("42") # => 42 (raises ArgumentError for non-numeric)Integer("abc") # => ArgumentError
# Number to string42.to_s # => "42"3.14.to_s # => "3.14"
# Float to integer (truncation vs rounding)3.7.to_i # => 3 (truncate)3.7.round # => 43.7.floor # => 33.7.ceil # => 4
# String parsing"hello world 42".scan(/\d+/) # => ["42"]"hello world".scan(/\w+/) # => ["hello", "world"]"key=value".split("=") # => ["key", "value"]Summary Table of Core Types
Section titled “Summary Table of Core Types”| Type | Example | Mutable | Notes |
|---|---|---|---|
| Integer | 42 | No | Arbitrary precision |
| Float | 3.14 | No | IEEE 754 double |
| Rational | 1/3r | No | Exact fractions |
| Complex | 3+4i | No | Complex arithmetic |
| String | "hello" | Yes | Use freeze for immutability |
| Symbol | :name | No | Interned identifiers |
| Array | [1, 2] | Yes | Ordered, indexed |
| Hash | { a: 1 } | Yes | Key-value, ordered |
| Range | 1..10 | Yes/No | Immutable endpoints, mutable iteration |
| Regexp | /pattern/ | No | Regular expressions |
| Proc | -> { } | Yes | Closures |
| Lambda | -> (x) { x } | Yes | Strict closures |
| nil | nil | No | Singleton NilClass |
| true | true | No | Singleton TrueClass |
| false | false | No | Singleton FalseClass |
Cross-References
Section titled “Cross-References”- Control Flow uses variable values and type checks in conditional branching and loop constructs.
- Methods and Blocks demonstrates how variables are passed to methods and blocks as parameters.
- Ruby Introduction provides the overview of Ruby’s dynamic typing system that governs how these types behave.
Common Mistakes
Section titled “Common Mistakes”- Confusing mutable and immutable objects: Strings in Ruby are mutable by default.
"hello".gsub!("l", "L")modifies the original string. Usefreezeor"hello".gsub("l", "L")(non-bang version) when you want to preserve the original value. - Misunderstanding truthiness: In Ruby, everything except
nilandfalseis truthy, including0,"", and[]. This catches beginners who expect0or empty strings to be falsy as in other languages. - Using
==vsequal?vseql?incorrectly:==checks value equality,equal?checks object identity (same object in memory), andeql?checks value and type. Use==for most comparisons;equal?is rarely needed in application code. - Mutating an argument inside a method: Passing a mutable object to a method and modifying it changes the original. Use
.dupor.freezeto prevent unintended side effects on the caller’s data.