Ruby Cheat Sheet
Every syntax, every method, every pattern. Searchable, version-tagged, copy-ready. Ruby 2.7 to 3.3.
name = "Alice" # local $global = "shared" # global @name = "Alice" # instance @@count = 0 # class MY_CONST = 42 # constant
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
x ||= "default" # assign if nil or false x &&= x.upcase # assign only if truthy
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]
user&.name # nil if user is nil x > 0 ? "pos" : "neg" result = begin risky_call() rescue "fallback" end
42 # Integer 3.14 # Float 1_000_000 # readable 0xFF # hex = 255 0b1010 # binary = 10 (1+2i) # Complex
(1..5) # inclusive: 1,2,3,4,5 (1...5) # exclusive: 1,2,3,4 (..5) # beginless (1..) # endless
42.class # Integer 42.is_a?(Numeric) # true 42.respond_to?(:to_s) # true 42.instance_of?(Integer) # true
puts "Hello" # with newline print "Hello" # no newline p obj # inspect (debugging) pp obj # pretty print
__method__ # current method name __dir__ # current file directory __FILE__ # current file path __LINE__ # current line number caller # call stack array binding # current Binding
Integer("42") # 42 (raises on bad input)
Float("3.14") # 3.14
Array(nil) # []
Array([1,2]) # [1,2]'literal \n no interpolation'
"Hello #{1 + 1}, newline \n here"sql = <<~SQL
SELECT * FROM users
WHERE id = #{user_id}
SQL# frozen_string_literal: true # Add at top of file.
"%.2f" % 3.14159 # "3.14" "%05d" % 42 # "00042" "%s=%d" % ["x", 10] # "x=10"
"hi".center(10) # " hi " "hi".ljust(10, "-") # "hi--------" "hi".rjust(10, "0") # "00000000hi"
"hello".upcase # "HELLO" "HELLO".downcase # "hello" "hello world".capitalize # "Hello world" "Hello".swapcase # "hELLO"
"hi".empty? # false
"hello".include?("ell") # true
"hello".start_with?("he") # true
"hello".end_with?("lo") # true" 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"]"hello".sub("l","r") # "herlo"
"hello".gsub("l","r") # "herro"
"hello".gsub(/[aeiou]/,"*") # "h*ll*"
"hello".tr("aeiou","*") # "h*ll*"
"hello".delete("l") # "heo""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)
[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]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
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
[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
[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[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{ "name" => "Alice" } # string keys
{ name: "Alice", age: 30 } # symbol keys
Hash.new(0) # default 0
Hash.new { |h,k| h[k] = [] }name = "Alice"; age = 30
{ name:, age: }
# same as { name: name, age: age }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")
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.valuesh.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 }if x > 0 "positive" elsif x < 0 "negative" else "zero" end puts "ok" if condition puts "bad" unless condition
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
"hello"
.then { |s| s.upcase }
.then { |s| "#{s}!" }
# "HELLO!"
42.yield_self { |n| n * 2 } # 84case { name: "Alice", age: 30 }
in { name: String => name, age: (18..) }
puts "Adult: #{name}"
endcase [1, 2, "foo", 3]
in [*, String => s, *]
puts "Found string: #{s}"
end{ name: "Alice" } => { name: String => name }
puts name # "Alice"while x < 10 x += 1 end until x == 10 x += 1 end loop do break if gets.chomp == "quit" end
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 }def greet(name)
"Hello, #{name}!"
end
def double(x) = x * 2
def square(x) = x ** 2def valid?(x) = x > 0 # returns boolean def normalize!(str) # mutates receiver str.strip! str.downcase! end
def measure start = Time.now result = yield [result, Time.now - start] end def maybe return yield if block_given? "no block" end
def full(pos, opt=1, *rest, key:, kopt: 0, **opts) [pos, opt, rest, key, kopt, opts] end
[1,2,3].map { _1 * 2 } # [2,4,6]
[[1,2],[3,4]].map { _1 + _2 } # [3,7]def wrap(...) inner(...) # forwards all args + block end
args = [1, 2, 3]
kwargs = { key: "k" }
method(*args)
method(**kwargs)
method(*args, **kwargs)[1,2,3].each { |n| puts n }
[1,2,3].each do |n|
puts n * 2
endsq = 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["1","2"].map(&:to_i) [1,2,3].each(&method(:puts))
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]e = [1,2].each + [3,4].each e.to_a # [1,2,3,4] [1,2].chain([3,4]).to_a
(1..Float::INFINITY).lazy
.select(&:odd?)
.map { |n| n**2 }
.first(5)
# [1,9,25,49,81]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? }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 }arr.sum
arr.sum { |n| n*2 }
arr.min_by { |n| -n }
arr.max_by(&:length)
arr.chunk { |n| n.even? }.to_a
arr.tallyclass 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 # 1class 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, ...]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}Measure = Data.define(:amount, :unit) m = Measure.new(amount: 100, unit: "km") m.frozen? # true m2 = m.with(amount: 200)
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
class Account def balance = compute_balance # public protected def balance_value = @balance private def compute_balance = @balance - @pending end
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? # truemodule Greetable
def greet = "Hello, I'm #{name}"
end
class Person
include Greetable
attr_reader :name
def initialize(name) = (@name = name)
end
Person.new("Alice").greetmodule ClassMethods
def description = "I am #{name}"
end
class Widget
extend ClassMethods
end
Widget.descriptionmodule Logging
def save
puts "Before"
result = super
puts "After"
result
end
end
class Record
prepend Logging
def save = (puts "Saving"; true)
end
Record.new.savemodule Payment
class Gateway
def charge(amt) = "Charging #{amt}"
end
TIMEOUT = 30
end
Payment::Gateway.new.charge(100)
Payment::TIMEOUT # 30class 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")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"
endvalue = Integer("abc") rescue 0
def parse(raw)
JSON.parse(raw)
rescue JSON::ParserError
{}
ensure
cleanup
endraise "Something went wrong" raise ArgumentError, "bad argument" raise # re-raise current exception
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}"
endattempts = 0
begin
attempts += 1
response = http_get("https://api.example.com")
rescue Net::TimeoutError => e
raise if attempts >= 3
sleep(2 ** attempts)
retry
end"hello" =~ /ell/ # 1 (position) or nil "hello" !~ /xyz/ # true "hello".match?(/ell/) # true (fastest) "hello".match(/e(ll)/) # MatchData $1 # "ll" (capture) $& # full match
m = "2024-01-15".match(
/(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/
)
m[:year] # "2024"
m[:month] # "01"
m.named_captures"one1two2".scan(/\d+/) # ["1","2"]
"hello world".gsub(/\b\w/) { |c| c.upcase }
"a1b2c3".split(/\d/) # ["a","b","c"]\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^ $ # 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
File.read("file.txt")
File.readlines("file.txt")
File.readlines("file.txt", chomp: true)File.foreach("file.txt") { |l| puts l.chomp }
File.open("file.txt", "r") do |f|
while (line = f.gets)
puts line
end
endFile.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.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.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 }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")
obj.send(:method_name, arg) obj.public_send(:method_name, arg) "hello".send(:upcase) # "HELLO"
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
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")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"String.class_eval do def palindrome? = self == self.reverse end "racecar".palindrome? # true
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.new
counter = 0
100.times.map do
Thread.new { mutex.synchronize { counter += 1 } }
end.each(&:join)
puts counter # reliably 100queue = 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 = 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 # 1results = (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 # 42require 'json'
JSON.parse('{"name":"Alice"}')
JSON.parse(raw, symbolize_names: true)
{ name: "Alice" }.to_json
JSON.pretty_generate({ name: "Alice" })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]
endrequire 'net/http'
uri = URI("https://api.example.com/users")
res = Net::HTTP.get_response(uri)
res.code # "200"
JSON.parse(res.body)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)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")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) # truerequire 'securerandom'
SecureRandom.uuid
SecureRandom.hex(16)
SecureRandom.base64
require 'benchmark'
Benchmark.bm do |x|
x.report("loop:") { 1_000_000.times { } }
endWhat 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 |
|
Arbitrary precision in Ruby 3.x |
|
Float |
|
IEEE 754 double precision |
|
String |
|
Mutable by default; |
|
Symbol |
|
Immutable; one instance per name in memory |
|
Boolean |
|
Instances of TrueClass / FalseClass |
|
Nil |
|
Single instance of NilClass |
|
Array |
|
Ordered, integer-indexed, mixed types |
|
Hash |
|
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 |
|
Current method or block |
|
Instance |
|
Object instance (all methods) |
|
Class |
|
Class and all its instances |
|
Global |
|
Entire program |
|
Constant |
|
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, or1- the basis of.sortandComparable -
&.(safe navigation): calls the method only if the receiver is notnil -
||=: assigns only if the left side isnilorfalse -
&&=: assigns only if the left side is truthy -
**: exponentiation (2 ** 10returns1024)
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 tocontinuein 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:
-
returnexits the enclosing method - not just the block -
breakexits the iterator and returns a value from it -
nextskips 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:
-
Required positional
-
Optional positional (with defaults)
-
Splat (
*args) -
Required keyword
-
Optional keyword (with defaults)
-
Double splat (
**kwargs) -
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 |
|---|---|---|
|
|
Getter only |
State is read externally, never written |
|
|
Setter only |
State is written externally, never read |
|
|
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 (\nor\r\n), used aftergets -
.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 |
|---|---|---|
|
|
Elements where block is truthy |
Filtering a subset |
|
|
Elements where block is falsy |
Inverse filter |
|
|
Transformed truthy results |
Map + select in one pass |
|
|
First matching element |
Single result needed |
|
|
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""fornil; 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 |
|---|---|---|
|
|
|
|
|
|
built-in |
|
|
|
built-in |
|
|
|
|
|
|
|
|
|
|
|
|
|
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.