cheat sheet

Ruby Cheat Sheet

Every syntax, every method, every pattern. Searchable, version-tagged, copy-ready. Ruby 2.7 to 3.3.

/ to focus
version tags: 2.7 3.0 3.1 3.2 = introduced in that version
Variables & Constantsbasic
Scopes at a glance
name     = "Alice"   # local
$global  = "shared"  # global
@name    = "Alice"   # instance
@@count  = 0         # class
MY_CONST = 42        # constant
Multiple & swap assignment
a, b, c  = 1, 2, 3
x, *rest = [1, 2, 3, 4]  # rest=[2,3,4]
*head, z = [1, 2, 3, 4]  # head=[1,2,3]
a, b     = b, a           # swap
Conditional assignment
x ||= "default"   # assign if nil or false
x &&= x.upcase    # assign only if truthy
Operatorsbasic
Arithmetic
5 + 2   # 7      5 - 2   # 3
5 * 2   # 10     5 / 2   # 2 (integer)
5.0 / 2 # 2.5    5 % 2   # 1
5 ** 2  # 25     -7.divmod(3)  # [-3, 2]
Safe navigation & ternary
user&.name          # nil if user is nil
x > 0 ? "pos" : "neg"
result = begin
  risky_call()
rescue
  "fallback"
end
Core Typesbasic
Numbers & literals
42        # Integer
3.14      # Float
1_000_000 # readable
0xFF      # hex = 255
0b1010    # binary = 10
(1+2i)    # Complex
Ranges
(1..5)    # inclusive: 1,2,3,4,5
(1...5)   # exclusive: 1,2,3,4
(..5)     # beginless
(1..)     # endless
Type checking
42.class              # Integer
42.is_a?(Numeric)     # true
42.respond_to?(:to_s) # true
42.instance_of?(Integer) # true
Output, Input & Kernelbasic
Output
puts "Hello"   # with newline
print "Hello"  # no newline
p obj          # inspect (debugging)
pp obj         # pretty print
Kernel introspection helpers
__method__   # current method name
__dir__      # current file directory
__FILE__     # current file path
__LINE__     # current line number
caller       # call stack array
binding      # current Binding
Strict conversion helpers
Integer("42")   # 42 (raises on bad input)
Float("3.14")   # 3.14
Array(nil)      # []
Array([1,2])    # [1,2]
String Literalsbasic
Single vs double quotes
'literal \n no interpolation'
"Hello #{1 + 1}, newline \n here"
Heredoc (squiggly)
sql = <<~SQL
  SELECT * FROM users
  WHERE id = #{user_id}
SQL
frozen_string_literal 2.7
# frozen_string_literal: true
# Add at top of file.
Formattingbasic
sprintf / % operator
"%.2f"  % 3.14159   # "3.14"
"%05d"  % 42         # "00042"
"%s=%d" % ["x", 10]  # "x=10"
Padding
"hi".center(10)       # "    hi    "
"hi".ljust(10, "-")   # "hi--------"
"hi".rjust(10, "0")   # "00000000hi"
Essential String Methodsbasic
Case
"hello".upcase        # "HELLO"
"HELLO".downcase      # "hello"
"hello world".capitalize  # "Hello world"
"Hello".swapcase      # "hELLO"
Check & search
"hi".empty?               # false
"hello".include?("ell")   # true
"hello".start_with?("he") # true
"hello".end_with?("lo")   # true
Strip / split / join
"  hi  ".strip       # "hi"
"hello\n".chomp      # "hello"
"a,b,c".split(",")   # ["a","b","c"]
["a","b"].join("-")  # "a-b"
"hello".chars        # ["h","e","l","l","o"]
Replace
"hello".sub("l","r")        # "herlo"
"hello".gsub("l","r")       # "herro"
"hello".gsub(/[aeiou]/,"*") # "h*ll*"
"hello".tr("aeiou","*")     # "h*ll*"
"hello".delete("l")         # "heo"
Slice & convert
"hello"[0]      # "h"
"hello"[1..3]   # "ell"
"hello"[-1]     # "o"
"42".to_i       # 42
"3.14".to_f     # 3.14
42.to_s(2)      # "101010" (binary)
Create & Accessbasic
Create
[1, 2, 3]
Array.new(3, 0)           # [0,0,0]
Array.new(3) { |i| i*2 } # [0,2,4]
%w[apple banana cherry]   # string array
%i[foo bar baz]           # symbol array
(1..5).to_a               # [1,2,3,4,5]
Access
a = [10,20,30,40,50]
a[0]        # 10
a[-1]       # 50
a[1..3]     # [20,30,40]
a[1, 2]     # [20,30] (start,length)
a.first(2)  # [10,20]
a.sample    # random element
Modifybasic
Add & remove
a.push(5)        # append
a << 6           # shovel
a.unshift(0)     # prepend
a.pop            # remove & return last
a.shift          # remove & return first
a.delete(20)     # remove by value
a.delete_at(1)   # remove by index
flatten / compact / uniq / set ops
[1,[2,[3]]].flatten      # [1,2,3]
[1,nil,2,nil].compact    # [1,2]
[1,1,2,3].uniq           # [1,2,3]
a = [1,2,3]; b = [2,3,4]
a | b  # [1,2,3,4]  union
a & b  # [2,3]      intersection
a - b  # [1]        difference
Search & Sortbasic
Search
[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].count { |n| n > 1 } # 2
[1,2,3].find  { |n| n > 1 } # 2
[1,2,3].index(2)             # 1
Sort & transform
[3,1,2].sort
[3,1,2].sort { |a,b| b <=> a }
words.sort_by(&:length)
[1,2,3].map { |n| n*2 }
[[1,2],[3,4]].flat_map { |a| a }
[1,2,3,4].group_by(&:even?)
[1,2,3,4].partition(&:even?)
["a","a","b"].tally
Create & Accessbasic
Literals
{ "name" => "Alice" }        # string keys
{ name: "Alice", age: 30 }  # symbol keys
Hash.new(0)                  # default 0
Hash.new { |h,k| h[k] = [] }
Hash shorthand 3.1
name = "Alice"; age = 30
{ name:, age: }
# same as { name: name, age: age }
Read & write
h[:name]
h.fetch(:name)             # raises if missing
h.fetch(:x, "default")
h.dig(:user, :address)     # nested safe
h[:email] = "a@b.com"
h.delete(:age)
h.merge(role: "admin")
h.merge!(role: "admin")
Iterate & Transformblock
Iterate
h.each { |k,v| puts "#{k}: #{v}" }
h.each_key   { |k| puts k }
h.each_value { |v| puts v }
h.key?(:name)
h.value?("Bob")
h.keys
h.values
select / reject / transform
h.select { |k,v| v > 18 }
h.reject { |k,v| v.nil? }
h.transform_values { |v| v.to_s }
h.transform_keys  { |k| k.to_s }
h.map { |k,v| [k, v.to_s] }.to_h
h.min_by { |k,v| v }
h.sort_by { |k,v| v }
Conditionalsbasic
if / elsif / else / unless
if x > 0
  "positive"
elsif x < 0
  "negative"
else
  "zero"
end

puts "ok" if condition
puts "bad" unless condition
case / when
case score
when 90..100  then "A"
when 80...90  then "B"
when Integer  then "some int"
when /error/i then "error matched"
else               "unknown"
end
then / yield_self pipeline
"hello"
  .then { |s| s.upcase }
  .then { |s| "#{s}!" }
# "HELLO!"

42.yield_self { |n| n * 2 }  # 84
Pattern Matching 3.0advanced
Hash pattern
case { name: "Alice", age: 30 }
in { name: String => name, age: (18..) }
  puts "Adult: #{name}"
end
Find pattern
case [1, 2, "foo", 3]
in [*, String => s, *]
  puts "Found string: #{s}"
end
One-line pattern
{ name: "Alice" } => { name: String => name }
puts name  # "Alice"
Loopsbasic
while / until / loop
while x < 10
  x += 1
end

until x == 10
  x += 1
end

loop do
  break if gets.chomp == "quit"
end
Loop control & numeric iterators
next      # skip to next iteration
break     # exit loop
break 42  # exit with value
redo      # restart iteration

3.times    { puts "hi" }
1.upto(5)  { |i| print i }
1.step(10,2) { |i| print i }
Defining Methodsbasic
Basic & endless 3.0
def greet(name)
  "Hello, #{name}!"
end

def double(x) = x * 2
def square(x) = x ** 2
? and ! conventions
def valid?(x) = x > 0   # returns boolean

def normalize!(str)      # mutates receiver
  str.strip!
  str.downcase!
end
yield & block_given?
def measure
  start = Time.now
  result = yield
  [result, Time.now - start]
end

def maybe
  return yield if block_given?
  "no block"
end
Argumentsbasic
Default, splat, keyword, double-splat
def full(pos, opt=1, *rest, key:, kopt: 0, **opts)
  [pos, opt, rest, key, kopt, opts]
end
Numbered block params 2.7
[1,2,3].map { _1 * 2 }         # [2,4,6]
[[1,2],[3,4]].map { _1 + _2 }  # [3,7]
Forwarding (...) 2.7
def wrap(...)
  inner(...)  # forwards all args + block
end
Spread on call
args   = [1, 2, 3]
kwargs = { key: "k" }
method(*args)
method(**kwargs)
method(*args, **kwargs)
Blocks, Procs & Lambdasbasic
Block syntax
[1,2,3].each { |n| puts n }
[1,2,3].each do |n|
  puts n * 2
end
Proc vs Lambda
sq = proc { |x| x**2 }
sq.call(4)   # 16

db = ->(x) { x*2 }
db.call(5)   # 10
db.(5)       # 10
db[5]        # 10

db.lambda?   # true
sq.lambda?   # false
Symbol-to-proc & method refs
["1","2"].map(&:to_i)
[1,2,3].each(&method(:puts))
Enumerator & Lazyadvanced
Enumerator
enum = [1,2,3].each
enum.next    # 1
enum.next    # 2
enum.rewind

fib = Enumerator.new do |y|
  a, b = 0, 1
  loop { y << a; a, b = b, a+b }
end
fib.take(7)  # [0,1,1,2,3,5,8]
Enumerator::Chain 2.7
e = [1,2].each + [3,4].each
e.to_a   # [1,2,3,4]

[1,2].chain([3,4]).to_a
Lazy enumerators
(1..Float::INFINITY).lazy
  .select(&:odd?)
  .map { |n| n**2 }
  .first(5)
# [1,9,25,49,81]
Enumerable Methodsblock
Core transforms
arr.map    { |n| n*2 }
arr.select { |n| n.even? }
arr.reject { |n| n.even? }
arr.reduce(0) { |s,n| s+n }
arr.flat_map { |n| [n,n] }
arr.filter_map { |n| n*2 if n.odd? }
each variants
arr.each_with_index { |v,i| puts "#{i}: #{v}" }
arr.each_with_object({}) { |n,h| h[n]=n**2 }
arr.each_slice(2) { |s| p s }
arr.each_cons(2)  { |c| p c }
Aggregations
arr.sum
arr.sum { |n| n*2 }
arr.min_by { |n| -n }
arr.max_by(&:length)
arr.chunk { |n| n.even? }.to_a
arr.tally
Class Definitionbasic
Full class
class Animal
  attr_accessor :name, :age
  attr_reader   :species
  @@count = 0

  def initialize(name, age, species)
    @name = name; @age = age
    @species = species; @@count += 1
  end

  def self.count = @@count
  def to_s = "#{@name} (#{@species})"
end

a = Animal.new("Rex", 5, "Dog")
Animal.count  # 1
Inheritance
class Dog < Animal
  def initialize(name, age)
    super(name, age, "Dog")
  end
  def speak = "Woof!"
end

rex = Dog.new("Rex", 3)
rex.is_a?(Animal)  # true
Dog.superclass     # Animal
Dog.ancestors      # [Dog, Animal, ...]
Struct, Data & Comparableblock
Struct
Point = Struct.new(:x, :y) do
  def distance_to(other)
    Math.sqrt((x-other.x)**2 + (y-other.y)**2)
  end
end
p = Point.new(3,4)
p.to_h  # {x:3, y:4}
Data - immutable 3.2
Measure = Data.define(:amount, :unit)
m = Measure.new(amount: 100, unit: "km")
m.frozen?   # true
m2 = m.with(amount: 200)
Comparable mixin
class Temperature
  include Comparable
  attr_reader :degrees
  def initialize(d) = (@degrees = d)
  def <=>(other) = degrees <=> other.degrees
end
temps = [Temperature.new(30), Temperature.new(20)]
temps.sort
temps.min
Visibility & Open Classesbasic
Method visibility
class Account
  def balance = compute_balance  # public

  protected
  def balance_value = @balance

  private
  def compute_balance = @balance - @pending
end
Open classes & Refinements
class Integer
  def factorial
    return 1 if self <= 1
    self * (self-1).factorial
  end
end
5.factorial  # 120

module StringExtras
  refine String do
    def palindrome? = self == self.reverse
  end
end
using StringExtras
"racecar".palindrome?  # true
include / extend / prependbasic
include - adds instance methods
module Greetable
  def greet = "Hello, I'm #{name}"
end
class Person
  include Greetable
  attr_reader :name
  def initialize(name) = (@name = name)
end
Person.new("Alice").greet
extend - adds class methods
module ClassMethods
  def description = "I am #{name}"
end
class Widget
  extend ClassMethods
end
Widget.description
prepend - inserts before class
module Logging
  def save
    puts "Before"
    result = super
    puts "After"
    result
  end
end
class Record
  prepend Logging
  def save = (puts "Saving"; true)
end
Record.new.save
Namespace & Custom Enumerableblock
Module as namespace
module Payment
  class Gateway
    def charge(amt) = "Charging #{amt}"
  end
  TIMEOUT = 30
end
Payment::Gateway.new.charge(100)
Payment::TIMEOUT  # 30
Custom Enumerable
class WordCollection
  include Enumerable
  def initialize = (@words = [])
  def add(w) = tap { @words << w }
  def each(&block) = @words.each(&block)
end
wc = WordCollection.new
wc.add("ruby").add("is").add("fun")
wc.map(&:upcase)
wc.sort
wc.include?("ruby")
rescue / ensure / elsebasic
Full rescue block
begin
  result = 10 / 0
rescue ZeroDivisionError => e
  puts "Division: #{e.message}"
rescue TypeError, ArgumentError => e
  puts "Type/Arg: #{e.message}"
rescue => e
  puts "Unknown: #{e}"
else
  puts "Success: #{result}"
ensure
  puts "Always runs"
end
Inline & method-level rescue
value = Integer("abc") rescue 0

def parse(raw)
  JSON.parse(raw)
rescue JSON::ParserError
  {}
ensure
  cleanup
end
raise & Custom Exceptionsblock
raise forms
raise "Something went wrong"
raise ArgumentError, "bad argument"
raise   # re-raise current exception
Custom exception hierarchy
class AppError < StandardError
  attr_reader :code
  def initialize(msg="app error", code: 500)
    super(msg); @code = code
  end
end
class NotFoundError < AppError
  def initialize(r)
    super("#{r} not found", code: 404)
  end
end
begin
  raise NotFoundError.new("User")
rescue AppError => e
  puts "#{e.code}: #{e.message}"
end
retry with backoff
attempts = 0
begin
  attempts += 1
  response = http_get("https://api.example.com")
rescue Net::TimeoutError => e
  raise if attempts >= 3
  sleep(2 ** attempts)
  retry
end
Matchingbasic
Match operators
"hello" =~ /ell/        # 1 (position) or nil
"hello" !~ /xyz/        # true
"hello".match?(/ell/)   # true (fastest)
"hello".match(/e(ll)/)  # MatchData
$1                      # "ll" (capture)
$&                      # full match
Named captures
m = "2024-01-15".match(
  /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/
)
m[:year]   # "2024"
m[:month]  # "01"
m.named_captures
scan / gsub / split
"one1two2".scan(/\d+/)          # ["1","2"]
"hello world".gsub(/\b\w/) { |c| c.upcase }
"a1b2c3".split(/\d/)            # ["a","b","c"]
Pattern Referencereference
Character classes & quantifiers
\d  \D   # digit / non-digit
\w  \W   # word char / non-word
\s  \S   # whitespace / non-whitespace
\b       # word boundary
.        # any char except newline
*  +  ?  # 0+, 1+, 0 or 1 (greedy)
{3} {2,5} *?  # exact, range, non-greedy
Anchors, groups & flags
^  $       # line start/end
\A  \Z     # string start/end
(foo)      # capture group
(?:foo)    # non-capture group
(?=foo)    # lookahead
(?<=foo)   # lookbehind
foo|bar    # alternation
/hi/i /hi/m /hi/x  # flags
Reading Filesblock
All at once
File.read("file.txt")
File.readlines("file.txt")
File.readlines("file.txt", chomp: true)
Line by line & block form
File.foreach("file.txt") { |l| puts l.chomp }

File.open("file.txt", "r") do |f|
  while (line = f.gets)
    puts line
  end
end
Writing Filesblock
Write & append
File.write("out.txt", "content")
File.write("out.txt", "more\n", mode: "a")
File.open("out.txt", "w") { |f| f.puts "line" }
File.open("out.txt", "a") { |f| f.puts "more" }
File & Dir Utilitiesbasic
File checks & path
File.exist?("file.txt")
File.file?("file.txt")
File.directory?("path")
File.size("file.txt")
File.extname("script.rb")        # ".rb"
File.basename("/a/b/c.rb")       # "c.rb"
File.dirname("/a/b/c.rb")        # "/a/b"
File.join("a","b","c.rb")        # "a/b/c.rb"
File.expand_path("~/.bashrc")
File ops & Dir
File.rename("old.txt","new.txt")
File.delete("file.txt")
FileUtils.mkdir_p("a/b/c")
FileUtils.cp("src","dst")
FileUtils.rm_rf("dir")
Dir.pwd
Dir.glob("**/*.rb")
Dir.children(".")
Dir.chdir("/tmp") { Dir.pwd }
Introspectionadvanced
Object introspection
obj.class
obj.class.ancestors
obj.is_a?(String)
obj.respond_to?(:upcase)
obj.methods
obj.instance_variables
obj.instance_variable_get(:@name)
obj.instance_variable_set(:@name, "Bob")
send & public_send
obj.send(:method_name, arg)
obj.public_send(:method_name, arg)
"hello".send(:upcase)  # "HELLO"
freeze & ObjectSpace
str = "hello".freeze
str.frozen?    # true
str.dup        # unfrozen copy

require 'objspace'
ObjectSpace.each_object(String).count
ObjectSpace.memsize_of(obj)
GC.start
GC.stat
Dynamic Methodsadvanced
define_method
class Reporter
  %w[info warn error].each do |level|
    define_method("log_#{level}") do |msg|
      puts "[#{level.upcase}] #{msg}"
    end
  end
end
Reporter.new.log_info("started")
method_missing + respond_to_missing?
class Ghost
  def method_missing(name, *args, &block)
    if name.to_s.start_with?("say_")
      puts name.to_s.sub("say_","")
    else
      super
    end
  end
  def respond_to_missing?(name, priv=false)
    name.to_s.start_with?("say_") || super
  end
end
Ghost.new.say_hello  # "hello"
class_eval
String.class_eval do
  def palindrome? = self == self.reverse
end
"racecar".palindrome?  # true
Threads & Mutexadvanced
Create & join
t = Thread.new { sleep 0.1; "done" }
t.join
t.value   # "done"

threads = (1..5).map { |i| Thread.new { i*2 } }
results = threads.map(&:value)
Mutex
mutex   = Mutex.new
counter = 0
100.times.map do
  Thread.new { mutex.synchronize { counter += 1 } }
end.each(&:join)
puts counter  # reliably 100
Queue (thread-safe)
queue = Queue.new
producer = Thread.new do
  5.times { |i| queue << i }
  queue << :done
end
consumer = Thread.new do
  loop do
    item = queue.pop
    break if item == :done
    puts item
  end
end
[producer, consumer].each(&:join)
Fiber & Ractoradvanced
Fiber (cooperative)
fiber = Fiber.new do
  puts "Step 1"
  Fiber.yield "yielded"
  puts "Step 2"
  "final"
end
fiber.resume
fiber.resume

counter = Fiber.new do
  i = 0
  loop { Fiber.yield i; i += 1 }
end
counter.resume  # 0
counter.resume  # 1
Ractor 3.0
results = (1..4).map do |i|
  Ractor.new(i) { |n| n**2 }
end.map(&:take)
# [1, 4, 9, 16] in parallel

r = Ractor.new { Ractor.receive * 2 }
r.send(21)
r.take  # 42
JSON & CSVbasic
JSON
require 'json'
JSON.parse('{"name":"Alice"}')
JSON.parse(raw, symbolize_names: true)
{ name: "Alice" }.to_json
JSON.pretty_generate({ name: "Alice" })
CSV
require 'csv'
CSV.foreach("data.csv", headers: true) do |row|
  puts row["name"]
end
rows = CSV.read("data.csv", headers: true)
CSV.open("out.csv","w") do |csv|
  csv << ["name","age"]
  csv << ["Alice", 30]
end
Net::HTTPblock
GET
require 'net/http'
uri = URI("https://api.example.com/users")
res = Net::HTTP.get_response(uri)
res.code  # "200"
JSON.parse(res.body)
POST with JSON
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
req = Net::HTTP::Post.new(uri)
req["Content-Type"] = "application/json"
req.body = { name: "Alice" }.to_json
res  = http.request(req)
Date, Time, Set & Morebasic
Time & Date
Time.now
Time.now.utc
Time.now.strftime("%Y-%m-%d")
Time.now + 86_400   # +1 day

require 'date'
Date.today
Date.today + 30
Date.parse("2024-01-15")
Set
require 'set'
s1 = Set[1,2,3]; s2 = Set[2,3,4]
s1 | s2  # Set{1,2,3,4}
s1 & s2  # Set{2,3}
s1 - s2  # Set{1}
s1.include?(2)  # true
SecureRandom & Benchmark
require 'securerandom'
SecureRandom.uuid
SecureRandom.hex(16)
SecureRandom.base64

require 'benchmark'
Benchmark.bm do |x|
  x.report("loop:") { 1_000_000.times { } }
end

What Is a Ruby Cheat Sheet?

A Ruby cheat sheet is a condensed syntax reference covering core Ruby constructs - variables, data types, operators, control flow, methods, classes, and iterators - designed for fast lookup during active development.

It is not a tutorial. No progressive hand-holding, no narrative. Every entry delivers the syntax, the behavior, and nothing else.

Ruby ranked 14th most popular programming language as of July 2024 with a 1.16% rating (Bacancy Technology), and Ruby on Rails powers the backends of GitHub, Shopify, and Airbnb - which means a large portion of working developers encounter Ruby syntax regularly, even if Ruby is not their primary language.

This reference covers Ruby's object model, string interpolation, array and hash methods, error handling, iterators, and file I/O. It targets 3 audiences:

  • Developers switching from Python or JavaScript who need syntax parity fast

  • Beginners past "Hello World" who need a structured reference without narrative padding

  • Intermediate developers who write Ruby weekly but still look up edge cases

This cheat sheet differs from the python cheat sheet or the javascript cheat sheet only in the language constructs covered - the reference format serves the same function across all three.


What Are the Core Data Types in Ruby?

Ruby has 8 core data types: Integer, Float, String, Symbol, Boolean (TrueClass/FalseClass), NilClass, Array, and Hash. Each has a distinct literal syntax and memory behavior that affects how you write and debug Ruby code.

Everything in Ruby is an object, including integers and nil. There are no primitive types.

Data Type

Literal Syntax

Key Behavior

Integer

42, -7, 1_000

Arbitrary precision in Ruby 3.x

Float

3.14, -0.5

IEEE 754 double precision

String

"hello", 'hello'

Mutable by default; freeze makes immutable

Symbol

:name, :"key-name"

Immutable; one instance per name in memory

Boolean

true, false

Instances of TrueClass / FalseClass

Nil

nil

Single instance of NilClass

Array

[1, "two", :three]

Ordered, integer-indexed, mixed types

Hash

{ key: value }

Key-value store; keys are usually Symbols

Strings and Symbols

Strings allocate a new object every time they appear. "name" on line 3 and "name" on line 7 are two different objects in memory.

Symbols are frozen and interned. :name always refers to the same object, which makes Symbols the correct choice for Hash keys where the key never changes.

"name".object_id == "name".object_id  # => false
:name.object_id  == :name.object_id   # => true

Use # frozen_string_literal: true at the top of any file where strings are used as constants - it reduces object allocation without changing behavior.

Arrays and Hashes

Arrays use zero-based integer indexing. Negative indexes count from the end: arr[-1] returns the last element.

Hashes default to nil for missing keys. Hash.new(0) sets a default value of 0 instead, which is the standard pattern for counting occurrences.

arr  = [10, 20, 30]
arr[-1]          # => 30

counter = Hash.new(0)
counter[:hits]  += 1   # => 1 (no KeyError)

Numbers, Booleans, and Nil

Ruby's Integer class has no size ceiling. 2 ** 1000 returns an exact result - no overflow, no BigDecimal required.

Truthiness rule: only false and nil are falsy. 0, "", and [] are all truthy. This differs from JavaScript and Python and is the most common source of logic bugs for developers switching to Ruby.


What Are Ruby Variable Types and Their Scope Rules?

Ruby has 5 variable types, each with a distinct naming prefix and scope boundary. Scope determines where a variable is readable and writable - getting this wrong produces NameError or silent data corruption.

Variable Type

Prefix

Scope

Local

name (lowercase)

Current method or block

Instance

@name

Object instance (all methods)

Class

@@name

Class and all its instances

Global

$name

Entire program

Constant

NAME or Name

Class/module scope, accessible globally

Local Variables

Local variables are the default. They exist only inside the method or block where they are defined.

Passing a local variable into a block does not change its scope - the block can read it, but a new assignment inside the block creates a separate variable, not a modification of the outer one.

x = 10
[1, 2].each { |i| x = x + i }  # x IS modified here - blocks share outer scope
puts x  # => 13

The block-shares-outer-scope rule catches developers coming from languages where blocks are isolated scopes.

Instance and Class Variables

Instance variables (@name) are initialized to nil if read before assignment - no error, just nil.

Class variables (@@name) are shared across the class and all subclasses. Modifying @@count in a subclass modifies it in the parent class too. This makes class variables dangerous in inheritance hierarchies - most Ruby developers prefer class-level instance variables (@name defined at class scope using self) instead.

Constants

Constants are not truly immutable in Ruby. Reassigning a constant raises a warning, not an error. To enforce immutability, call .freeze on the value:

MAX_RETRIES = 3.freeze
ALLOWED_HOSTS = ["api.example.com", "cdn.example.com"].freeze

Global variables ($name) are accessible from anywhere and persist for the lifetime of the program. Ruby's standard library uses them ($stdout, $LOAD_PATH), but application code should treat them as a last resort - they make behavior impossible to trace across a large codebase.


What Are the Ruby Operators and Their Precedence Order?

Ruby has 6 operator categories: arithmetic, comparison, logical, assignment, bitwise, and Ruby-specific operators. Precedence determines evaluation order when operators appear in the same expression without parentheses.

Ruby-specific operators that appear in no other mainstream language:

  • <=> (spaceship): returns -1, 0, or 1 - the basis of .sort and Comparable

  • &. (safe navigation): calls the method only if the receiver is not nil

  • ||=: assigns only if the left side is nil or false

  • &&=: assigns only if the left side is truthy

  • **: exponentiation (2 ** 10 returns 1024)

Comparison Operators

5 == 5.0      # => true   (value equality)
5.eql?(5.0)   # => false  (type + value equality)
5.equal?(5)   # => true   (object identity - same object_id)

Key distinction: == checks value, .eql? checks value and type, .equal? checks object identity. Using the wrong one in conditionals produces bugs that are nearly invisible at a glance.

The spaceship operator <=> returns nil when objects are not comparable - not an error. sort raises ArgumentError if any comparison in the collection returns nil.

Logical Operators and Precedence

&& vs and: Both are logical AND, but they have different precedence. && binds tighter than assignment; and binds looser. This produces completely different results:

x = true && false   # x = false   (&& evaluated before =)
x = true and false  # x = true    (= evaluated before and)

Use &&, ||, ! in conditions. Reserve and, or, not for control flow where their low precedence is intentional.


What Is Ruby Control Flow Syntax?

Ruby's control flow constructs cover 4 categories: conditionals (if, unless, case), ternary expressions, short-circuit operators used as flow control, and postfix (inline) forms. Most have an inline variant that reads as natural English.

if / elsif / else

if score >= 90
  grade = "A"
elsif score >= 75
  grade = "B"
else
  grade = "C"
end

Postfix form (one-liner) works when there is no else:

send_alert if temperature > 100
retry_request unless response.ok?

The postfix form reads as a sentence. Use it for guard clauses and single-condition side effects - not for complex branching.

unless

unless is if !condition. Use it when the falsy case is the primary concern.

Wrong: if !user.nil? - double negation costs reading time. Right: unless user.nil? or better, if user (Ruby's truthiness makes nil checks implicit).

unless/else is valid but unusual. If you find yourself writing unless condition ... else, flip it to if condition ... else - the positive condition reads more clearly.

case / when

case/when in Ruby does more than integer switching. It matches against ranges, regular expressions, classes, and lambdas - any object that implements ===.

case input
when 1..10       then "single digit range"
when /^\d+$/     then "numeric string"
when String      then "some other string"
when ->(x) { x.nil? } then "nil input"
end

=== is the match operator that when uses internally. String === "hello" returns true. This makes case the idiomatic Ruby replacement for multiple is_a? checks.

Ternary and Inline Conditionals

label = count == 1 ? "item" : "items"

Ternary works for single-value assignment. When the expression spans more than one line, use if/else - multi-line ternaries are harder to read than the structure they replace.

Short-circuit as control flow:

user = find_user(id) || raise(UserNotFoundError)
config[:timeout] ||= 30

||= is the most-used assignment shortcut in Ruby code. It initializes a value only when the variable is nil or false, making it the standard pattern for default assignment and memoization.


What Are Ruby Loop and Iterator Constructs?

Ruby provides 2 categories of looping: imperative loops (loop, while, until, for) and Enumerable iterators (.each, .map, .select, .reduce). The Enumerable approach is idiomatic Ruby - imperative loops exist but appear far less often in production code.

RubyGems.org recorded over 4.15 billion total gem downloads in April 2025, a 51% increase from 2.74 billion in April 2024 (RubyGems Blog) - an ecosystem that size produces code patterns worth understanding. Enumerable iterators are at the center of virtually all of it.

Imperative Loops (loop, while, until)

loop do runs forever until break exits it. It is the correct construct when the exit condition is complex or appears mid-block.

loop do
  input = gets.chomp
  break if input == "quit"
  process(input)
end
  • while condition - runs while condition is truthy

  • until condition - runs while condition is falsy

  • next - skips to the next iteration (equivalent to continue in other languages)

  • break value - exits the loop and returns a value

for item in collection exists but is avoided in idiomatic Ruby. It does not create a new scope for the iteration variable - item leaks out of the loop. Use .each instead.

Enumerable Iterators (each, map, select, reduce)

The 4 most-used iterators:

  • .each - iterates, returns the original collection unchanged

  • .map - iterates, returns a new array of transformed values

  • .select - returns elements where the block returns truthy

  • .reduce - accumulates a single value across all elements

[1, 2, 3].map    { |n| n * 2 }        # => [2, 4, 6]
[1, 2, 3].select { |n| n.odd? }       # => [1, 3]
[1, 2, 3].reduce(0) { |sum, n| sum + n } # => 6

.each_with_index and .each_with_object handle cases where you need the index or need to build an accumulator object alongside iteration.

Bang methods (.map!, .select!) modify the original array in place. Non-bang methods return a new object and leave the original unchanged. Choosing the wrong one is a common source of bugs in data transformation pipelines.

Integer iterators .times, .upto, and .downto generate sequences without building an intermediate array:

5.times  { |i| puts i }       # 0, 1, 2, 3, 4
1.upto(5)  { |i| puts i }     # 1, 2, 3, 4, 5
5.downto(1) { |i| puts i }    # 5, 4, 3, 2, 1

return, break, and next inside blocks:

  • return exits the enclosing method - not just the block

  • break exits the iterator and returns a value from it

  • next skips to the next iteration

Confusing return and break inside .each blocks causes methods to exit unexpectedly. This is the most common control flow mistake in Ruby for developers coming from JavaScript forEach.


What Is Ruby Method Syntax and Argument Types?

Ruby methods support 6 argument types: required, default, keyword, required keyword, splat, and block. Mixing them in a single method definition follows a strict ordering rule - getting the order wrong raises a SyntaxError at parse time, not runtime.

Ruby proficiency with Rails resulted in 1.64 times more interview callbacks than without it (DevOps.com, 2024) - and method design is one of the first things interviewers test.

Defining Methods and Return Values

def method_name(arg)
  # body
end

Ruby returns the last evaluated expression implicitly. Explicit return exits early. Both are valid - the convention is to use implicit return unless exiting mid-method.

def absolute_value(n)
  return n if n >= 0
  -n
end

Methods ending in ? return a boolean by convention. Methods ending in ! mutate the receiver or raise an exception on failure. These are conventions Ruby enforces through community standards, not the interpreter.

Default and Keyword Arguments

Default arguments:

def greet(name = "World", greeting = "Hello")
  "#{greeting}, #{name}!"
end

Keyword arguments use a colon suffix in the method signature. They are passed by name, making argument order irrelevant at the call site:

def create_user(name:, role: "viewer", admin: false)
  # name is required; role and admin have defaults
end

create_user(name: "Dana", admin: true)

Required keyword arguments (no default) raise ArgumentError if missing. Optional keyword arguments (with default) can be omitted.

Splat and Double Splat

Splat (*args) collects extra positional arguments into an Array:

def log(level, *messages)
  messages.each { |m| puts "[#{level}] #{m}" }
end
log("ERROR", "Disk full", "Write failed", "Process halted")

Double splat (**kwargs) collects extra keyword arguments into a Hash:

def configure(**options)
  options.each { |k, v| puts "#{k}: #{v}" }
end
configure(timeout: 30, retries: 3)

Argument order rule when combining types:

  1. Required positional

  2. Optional positional (with defaults)

  3. Splat (*args)

  4. Required keyword

  5. Optional keyword (with defaults)

  6. Double splat (**kwargs)

  7. Block (&block)

Block Arguments and yield

Methods accept a block either implicitly via yield or explicitly by capturing it with &block:

def repeat(n)
  n.times { yield }
end
repeat(3) { puts "hello" }
def run(&block)
  block.call
end

block_given? checks whether a block was passed before calling yield - calling yield without a block raises LocalJumpError.

Shopify built its entire software development stack on Ruby, and block-based APIs like ActiveRecord query builders and middleware chains rely entirely on this pattern - understanding it is not optional for production Ruby work.


What Is Ruby Class and Object Syntax?

A Ruby class is a blueprint for creating objects. It bundles state (instance variables) and behavior (methods) into a single unit. The initialize method runs automatically when new is called.

The 2024 Ruby on Rails Community Survey gathered responses from over 2,700 developers across 106 countries (Planet Argon, 2024) - that community has converged on consistent class design conventions worth knowing cold.

Defining Classes and Constructors

class User
  def initialize(name, role = "viewer")
    @name = name
    @role = role
  end
end

user = User.new("Dana", "admin")

initialize is private by default. Calling User.initialize directly raises NoMethodError.

Every object in Ruby has a class and a singleton class. The singleton class holds object-specific methods, which is how def self.method_name creates class-level behavior without a module.

Attribute Accessors

Ruby generates getter and setter methods through 3 accessor macros:

Macro

Generates

Use When

attr_reader

Getter only

State is read externally, never written

attr_writer

Setter only

State is written externally, never read

attr_accessor

Both getter and setter

State is read and written externally

class Product
  attr_accessor :name, :price
  attr_reader   :sku

  def initialize(name, price, sku)
    @name  = name
    @price = price
    @sku   = sku
  end
end

attr_reader for sku enforces that SKUs are set once at instantiation and never overwritten from outside the class.

Inheritance and Access Control

class AdminUser < User
  def initialize(name)
    super(name, "admin")
  end

  def deactivate_account(target)
    target.status = :inactive
  end
end

super without arguments passes all arguments from the current method up to the parent. super() passes no arguments. The distinction matters when the child method has a different signature.

Ruby has 3 access modifiers:

  • public - callable from anywhere (default)

  • protected - callable from the defining class and its subclasses

  • private - callable only within the defining object, and never with an explicit receiver

private in Ruby is stricter than in most languages: self.private_method raises NoMethodError even from within the same class (before Ruby 2.7, which relaxed the self. restriction for private methods).

Basecamp's entire project management platform is built as a Rails monolith using these class patterns - and their team has publicly documented using private method boundaries as a substitute for microservice separation in their back-end development architecture.


What Are Ruby Modules and Mixins?

A Ruby module serves 2 distinct purposes: namespacing (grouping related constants and methods) and mixins (sharing behavior across classes without inheritance).

Modules cannot be instantiated. They have no new method.

Modules as Namespaces

module Payments
  class Processor
    def charge(amount); end
  end

  class Refunder
    def refund(transaction_id); end
  end

  TAX_RATE = 0.08
end

processor = Payments::Processor.new

Namespacing prevents name collisions in large codebases. Processor as a standalone class would conflict with any other Processor class in the project. Payments::Processor is unambiguous.

include vs extend vs prepend

Three keywords attach a module to a class, each placing it differently in the method lookup chain:

include - adds module methods as instance methods. Most common pattern.

extend - adds module methods as class methods. Used for class-level utilities like factory helpers.

prepend - inserts the module before the class in the lookup chain. Module methods run first, which enables wrapping or overriding class behavior without monkey patching.

module Timestamps
  def created_at_label
    "Created: #{@created_at}"
  end
end

class Order
  include Timestamps
end

SitePoint's 2024 module reference notes that prepend places the module directly below the class in the Ruby Object Model, making it the correct tool when module methods need to call super to access the original class behavior.

Comparable and Enumerable

Ruby's standard library ships 2 modules that unlock entire suites of functionality with minimal implementation:

Comparable - include it and define <=>. Ruby generates <, >, <=, >=, between?, and clamp automatically.

Enumerable - include it and define each. Ruby generates .map, .select, .reject, .find, .sort, .min, .max, .reduce, and 50+ other methods automatically.

class Temperature
  include Comparable
  attr_reader :degrees

  def initialize(d) = @degrees = d
  def <=>(other) = degrees <=> other.degrees
end

temps = [Temperature.new(98), Temperature.new(72), Temperature.new(103)]
temps.min.degrees  # => 72
temps.sort.map(&:degrees)  # => [72, 98, 103]

This is the most efficient path to a fully sortable, filterable custom object in Ruby - 3 lines of implementation unlocks the entire Comparable interface.


What Are Ruby String Methods and Interpolation Rules?

String interpolation and transformation sit at the center of nearly all Ruby output: API responses, log messages, template rendering, and data formatting. Ruby's String class has over 100 instance methods.

In Ruby 3.3, Shopify's core team identified and fixed a performance regression in short string interpolation - resolving a memory allocation issue introduced by variable width allocation in Ruby 3.2 (RubyKaigi 2024). String performance is actively maintained at the language level.

String Interpolation Syntax

Interpolation only works in double-quoted strings.

name = "Dana"
"Hello, #{name}!"        # => "Hello, Dana!"
'Hello, #{name}!'        # => "Hello, \#{name}!" (literal, not interpolated)
"Result: #{2 + 2}"       # => "Result: 4"
"Upcased: #{"hello".upcase}"  # => "Upcased: HELLO"

Any Ruby expression fits inside #{}. The result is converted to a string via .to_s. A nil inside interpolation produces an empty string, not an error.

Transformation Methods

  • .upcase / .downcase / .capitalize - case conversion

  • .strip - removes leading and trailing whitespace

  • .chomp - removes trailing newline (\n or \r\n), used after gets

  • .chop - removes the last character unconditionally

.gsub vs .sub: .gsub replaces all matches; .sub replaces the first match only.

"hello world hello".gsub("hello", "hi")  # => "hi world hi"
"hello world hello".sub("hello", "hi")   # => "hi world hello"

Heredoc and Frozen Strings

Heredoc syntax handles multiline strings without escaped newlines:

message = <<~HEREDOC
  Dear #{name},
  Your order has shipped.
HEREDOC

The ~ squiggly heredoc strips leading whitespace based on the least-indented line.

.freeze makes a string immutable. # frozen_string_literal: true applies .freeze to every string literal in the file - the standard optimization for files that use strings as constants rather than mutable values.

Before Ruby 3.3, String#+ (unary plus) was almost 2x faster than String#dup for unfreezing frozen strings (Saeloun, 2024). In Ruby 3.3, the performance gap closed.


What Are Ruby Array Methods for Transformation and Filtering?

Ruby's Array class implements the Enumerable module, which means the full suite of Enumerable methods applies. The Array methods below are either Array-specific or Enumerable methods commonly used on arrays.

98.3% of all Ruby code on GitHub lives in Rails projects (Gitnux, 2023) - Rails ActiveRecord collections use these exact Array and Enumerable patterns for query result manipulation.

Transformation Methods

.map / .map! - transforms each element; returns new array (or mutates in place with !).

.flat_map - maps and flattens one level. Replaces .map { }.flatten(1).

.compact / .compact! - removes nil values.

.flatten(depth) - flattens nested arrays. Optional depth argument limits how many levels are flattened.

[[1, 2], [3, [4, 5]]].flatten(1)   # => [1, 2, 3, [4, 5]]
[[1, 2], [3, [4, 5]]].flatten      # => [1, 2, 3, 4, 5]

Filtering and Searching

Method

Returns

Use When

.select

Elements where block is truthy

Filtering a subset

.reject

Elements where block is falsy

Inverse filter

.filter_map

Transformed truthy results

Map + select in one pass

.find

First matching element

Single result needed

.any? / .all? / .none?

Boolean

Checking conditions

.filter_map is the most useful addition in recent Ruby versions. It replaces .map { }.compact for cases where the transform step may return nil:

[1, 2, 3, 4, 5].filter_map { |n| n * 2 if n.odd? }  # => [2, 6, 10]

Aggregation Methods

Aggregation does not modify the array - it collapses it to a single value or sorted copy.

  • .sum - equivalent to .reduce(:+) but faster for numeric arrays

  • .min_by / .max_by - finds min or max by an attribute

  • .sort_by - stable sort by an attribute (preferred over .sort { |a, b| a.x <=> b.x })

  • .tally - counts occurrences of each element; returns a Hash

words = ["ruby", "python", "ruby", "go"]
words.tally  # => {"ruby"=>2, "python"=>1, "go"=>1}

.tally was added in Ruby 2.7 and replaces the Hash.new(0) counter pattern for most counting tasks.


What Are Ruby Hash Methods and Access Patterns?

A Ruby Hash stores key-value pairs with O(1) average-case lookup. Hashes maintain insertion order as of Ruby 1.9.

37% of Rails developers deploy to production multiple times per day (Planet Argon Rails Community Survey, 2024). Hash manipulation appears in nearly every request cycle in a Rails app - serialization, parameter access, configuration - making these patterns high-frequency in production code.

Hash Creation and Safe Access

# Literal
config = { timeout: 30, retries: 3, debug: false }

# Hash.new with default
counter = Hash.new(0)

# From array of pairs
Hash[[:a, 1, :b, 2]]  # => { a: 1, b: 2 }

.fetch vs []: hash[:missing_key] returns nil. hash.fetch(:missing_key) raises KeyError. Use .fetch when a missing key is a bug, not an expected case.

.dig safely traverses nested hashes without NoMethodError on nil:

config = { db: { host: "localhost", port: 5432 } }
config.dig(:db, :port)          # => 5432
config.dig(:db, :password)      # => nil (no error)
config.dig(:cache, :host)       # => nil (no error)

Transformation and Merging

  • .transform_keys - returns new hash with modified keys

  • .transform_values - returns new hash with modified values

  • .merge - returns new hash combining two hashes; later values win on key collision

  • .merge! / .update - merges in place

prices = { apple: 1.2, banana: 0.5 }
prices.transform_values { |v| (v * 1.1).round(2) }
# => { apple: 1.32, banana: 0.55 }

Merge with block handles collision resolution explicitly:

a = { x: 1 }
b = { x: 9, y: 2 }
a.merge(b) { |key, old, new_val| old + new_val }
# => { x: 10, y: 2 }

Iteration and Extraction

.each on a hash yields |key, value| pairs. .map returns an array of transformed pairs - to get a hash back, chain .to_h or use .each_with_object({}).

config.select { |k, v| v.is_a?(Integer) }  # hash of integer-valued keys only
config.keys    # => [:timeout, :retries, :debug]
config.values  # => [30, 3, false]
config.to_a    # => [[:timeout, 30], [:retries, 3], [:debug, false]]

What Is Ruby Error Handling Syntax?

Ruby's error handling uses a begin / rescue / else / ensure / end structure. Every clause is optional except rescue, which is the only mandatory addition to the bare begin / end block.

begin / rescue / ensure / end

begin
  result = risky_operation
rescue ArgumentError => e
  puts "Bad input: #{e.message}"
rescue NetworkError, TimeoutError => e
  retry_count += 1
  retry if retry_count < 3
  raise
else
  process(result)   # runs only if NO exception was raised
ensure
  cleanup_resources  # always runs, exception or not
end

else runs when no exception occurs - it makes the success path explicit without burying it in the begin block.

ensure runs unconditionally: after a successful run, after a rescued exception, and even after return inside the begin block. Use it for file handles, database connections, and any resource that must be released.

raise and Custom Exceptions

raise without arguments re-raises the current exception. Use this inside a rescue block after logging - it preserves the original stack trace.

class PaymentDeclinedError < StandardError
  def initialize(amount, reason)
    super("Payment of $#{amount} declined: #{reason}")
    @amount = amount
  end
end

raise PaymentDeclinedError.new(99.99, "insufficient funds")

Custom exception classes inherit from StandardError, not Exception. Never rescue Exception - it catches SignalException, SystemExit, and Interrupt, which Ruby uses internally for process termination.

The rule from the Ruby community: rescue the most specific exception class possible. Rescuing StandardError blindly hides NameError and NoMethodError, which are usually bugs, not expected conditions (Dalibor Nasevic).

retry

retry re-executes the begin block from the start. Always pair it with a counter or it creates an infinite loop.

attempts = 0
begin
  attempts += 1
  connect_to_api
rescue Faraday::TimeoutError
  retry if attempts < 3
  raise
end

3 retries is the standard pattern for transient network errors. On the fourth failure, raise re-raises Faraday::TimeoutError with the original message and backtrace intact.


What Are Ruby File I/O and Common Standard Library Methods?

Ruby's standard library ships with over 100 built-in classes. The ones below cover the patterns developers look up most - file reading and writing, output debugging, and dependency loading.

The 2024 Ruby on Rails Community Survey found 83% of respondents feel the Rails core team is shepherding the project in the right direction (Planet Argon, 2024), which includes the ongoing migration of standard library components to bundled gems - a structural change affecting how some library features are loaded in Ruby 3.x.

File Reading and Writing

Block form of File.open auto-closes the file when the block exits, even if an exception is raised.

# Read entire file
content = File.read("data.txt")

# Write (overwrites existing content)
File.write("output.txt", content)

# Block form - file is closed automatically
File.open("log.txt", "a") do |f|
  f.puts "Entry: #{Time.now}"
end

# Line-by-line without loading full file into memory
IO.foreach("large_file.csv") { |line| process(line) }

IO.foreach is the correct choice for large files. File.read loads the entire file into a String object - on a 2 GB log file, that exhausts memory. IO.foreach reads one line at a time.

require vs require_relative

  • require "library_name" - loads from Ruby's $LOAD_PATH; used for gems and standard library

  • require_relative "path/to/file" - loads relative to the current file's directory; used for local files

Mixing them up is a common source of LoadError in Ruby scripts that run correctly from one directory but fail from another.

Output Methods

3 output methods with distinct behaviors:

  • puts - adds a newline; prints "" for nil; calls .to_s

  • p - calls .inspect; shows the raw representation including quotes and type info; returns the object

  • pp - pretty-prints with indentation for nested structures; useful for hashes and arrays

str = "hello"
puts str   # => hello
p str      # => "hello"
pp [1, {a: 2}]  # => [1, {:a=>2}]   (formatted)

p returns its argument, which makes it valid inside method chains for debugging without breaking the chain: value.transform.tap { |v| p v }.further_transform.

Standard Library Highlights

For a web development IDE setup, knowing which libraries require explicit require calls saves debugging time.

Library

Load With

Key Methods

Date

require "date"

Date.today, Date.parse, date + 7

Time

built-in

Time.now, .strftime, .utc

Math

built-in

Math::PI, Math.sqrt, Math.log

JSON

require "json"

JSON.parse, JSON.generate, .to_json

CSV

require "csv"

CSV.read, CSV.foreach, CSV.generate

SecureRandom

require "securerandom"

.uuid, .hex, .base64

JSON and CSV ship with Ruby but require an explicit require statement - they are standard library, not built-in. Forgetting the require raises NameError: uninitialized constant JSON.

SecureRandom.uuid generates a version-4 UUID. Used alongside a UUID Generator tool, it covers both programmatic and manual UUID needs during development and testing.

Other syntax references worth bookmarking alongside this one: the sql cheat sheet for database query syntax, the regex cheat sheet for Ruby's Regexp class, and the git cheat sheet for version control commands used during Ruby project workflows.

FAQ on Ruby

What is the difference between a Symbol and a String in Ruby?

Symbols are immutable and interned - :name always references the same object in memory. Strings allocate a new object each time. Use Symbols for Hash keys and identifiers; use Strings for values that change or get manipulated.

How does string interpolation work in Ruby?

Interpolation uses #{} inside double-quoted strings only. Any Ruby expression fits inside the braces - variables, method calls, arithmetic. Single-quoted strings treat #{} as literal characters and never interpolate.

What is the difference between puts, p, and pp in Ruby?

puts calls .to_s and adds a newline. p calls .inspect, showing raw type information - useful for debugging. pp pretty-prints nested structures with indentation. All three serve debugging, but p is the fastest to use mid-chain.

How do Ruby modules differ from classes?

Modules cannot be instantiated or subclassed. They serve two purposes: namespacing (grouping constants and classes) and mixins (sharing behavior via include, extend, or prepend). A class inherits from one parent; it can include unlimited modules.

What does attr_accessor do in Ruby?

attr_accessor generates both a getter and a setter method for an instance variable. attr_reader generates only a getter; attr_writer only a setter. All three replace manual def name and def name=(val) method definitions.

How does Ruby error handling work?

Ruby uses begin / rescue / ensure / end. The rescue clause catches exceptions; ensure runs unconditionally regardless of outcome - correct for closing files or releasing database connections. Always rescue specific exception classes, never bare Exception.

What is the difference between map, select, and reduce in Ruby?

.map transforms each element and returns a new array. .select filters elements where the block returns truthy. .reduce collapses the collection into a single accumulated value. All three are Enumerable methods available on any class that implements each.

What are the Ruby variable scope types?

Ruby has 5 variable types: local (name), instance (@name), class (@@name), global ($name), and constant (NAME). Scope controls where each is readable. Instance variables default to nil if read before assignment - no NameError, just silent nil.

What is the safe navigation operator in Ruby?

Written as &., it calls a method only if the receiver is not nil. user&.email returns nil instead of raising NoMethodError when user is nil. It replaces common user && user.email patterns with a single operator.

How do you read and write files in Ruby?

Use File.read("path") for full file content and File.write("path", data) to overwrite. For large files, IO.foreach reads line by line without loading everything into memory. Always use block-form File.open - it closes the file handle automatically.