Design Philosophy and Coming from Other Languages

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Ruby 4.0 (4.0.7) and was checked on 2026-10-18. Features newer than Ruby 3.0 name the version that introduced them. It explains why Ruby looks the way it does, corrects a common misunderstanding about how a Ruby file is executed, and then maps the concepts a programmer already knows from another language onto their Ruby counterparts. The other pages of this reference go into each topic in depth; this one is the orientation that makes them easier to read.

Every example was run with Ruby 4.0.7 with warnings enabled (ruby -w), and the output shown is what it printed. Examples that start with require "shelf" use the small shelf library-catalogue gem that serves as the running scenario of this section (Shelf::Isbn, Shelf::Author, Shelf::Book, Shelf::Catalog; see the shelf gem source). The first page, Getting Started, shows how to install Ruby and run a file.

The design goals

Ruby was created by Yukihiro Matsumoto ("Matz") and first released in 1995. The language’s own materials present it as a dynamic, open source language that values simplicity and productivity. Matz has repeatedly said that the goal is to make programmers happy, not to make machines fast or compilers simple; where the two conflict, Ruby usually chooses the human. Most of what follows, and most of what surprises newcomers, is a consequence of that choice.

Idea What it means in practice

Programmer happiness

Code should read close to the intent. Ruby accepts a larger language (several ways to say the same thing, optional parentheses, unless, statement modifiers) in exchange for shorter, more expressive programs.

Everything is an object

Numbers, nil, classes, even true have methods. There are no primitive types and no boxing; operators are method calls.

Principle of least surprise

The language tries to behave the way an experienced Rubyist expects. Matz’s own caveat: the surprise is measured against his expectations, and yours may differ.

More than one way to do it

Ruby offers aliases and overlapping methods (size, length and count; map and collect) on purpose. Teams settle on one with a style guide and a linter.

Duck typing

An object is judged by what it can do (which messages it answers), not by its declared class or interface.

Open classes

Classes and modules are never closed: you can reopen any of them, including core ones, to add or replace methods.

Blocks and iterators

Control flow over collections is expressed by passing code (blocks) to methods, not by index loops.

Everything is an object

Every value is an instance of a class, and every class is itself an object, an instance of Class. There are no primitives, so there is no difference between 5 and Integer(5) and nothing to box or unbox. Operators are ordinary method calls with special syntax: 1 + 2 is 1.+(2).

# frozen_string_literal: true

require "shelf"

p 42.class
p nil.class
p :isbn.class
p (1..3).class
p Shelf::Book.class
p Shelf::Book.class.class
p 42.respond_to?(:+)
p 1.+(2)
p nil.to_a
p nil.to_s
p nil.inspect
p Integer.ancestors.first(4)
p BasicObject.superclass
p Class.superclass
p Module.superclass
p Object.superclass

Output:

Integer
NilClass
Symbol
Range
Class
Class
true
3
[]
""
"nil"
[Integer, Numeric, Comparable, Object]
nil
Module
Object
BasicObject

Source: Object, BasicObject and NilClass

Three consequences are worth knowing early:

  • nil is an object (the only instance of NilClass), so calling a method on it raises NoMethodError with a readable message rather than a null-pointer crash. nil.to_a, nil.to_s and nil.inspect are all defined and useful; the safe-navigation operator &. skips the call when the receiver is nil.

  • The hierarchy has no gaps: BasicObject (a nearly empty root) is the superclass of Object, and Class descends from Module, which descends from Object. A class is therefore an object that you can pass around, store in a hash or call methods on.

  • Because operators are methods, you define +, ==, <=>, [] or << for your own classes by writing methods with those names. See Operators and Precedence.

The Ruby way of writing code

A handful of conventions give Ruby code its recognisable look. All of them are optional; all of them are what you will find in nearly every library.

  • The last expression is the return value. A method ends with the value it computes; an explicit return is reserved for leaving early. Everything is an expression: if, case and begin all produce values.

  • Parentheses are optional for method calls with arguments, and are usually omitted for "command-like" calls (puts "x", raise Error, attr_reader :name) and for DSLs. They are kept when the call is nested, chained or ambiguous.

  • Method chaining reads left to right, with each call feeding the next; line breaks before a leading . are allowed.

  • Names carry meaning. A trailing ? marks a predicate that returns a boolean (empty?, include?), a trailing ! marks the more dangerous variant of a method (sort! mutates, sort copies), and a trailing = defines a setter. The suffixes are conventions, not syntax, and they are always part of the method name.

  • Prefer the iterator to the loop. select, map, sum, each_with_object and friends replace hand-written index loops and accumulator variables. &:name turns a method name into a block, and Ruby 3.4 added the implicit block parameter it.

  • Ask the collection. size and length are the same, count also accepts a block, empty? is preferred over comparing the size with zero, and any?, all?, none? and include? replace manual searches.

# frozen_string_literal: true

require "shelf"

isbn = Shelf::Isbn.new("978-0-306-40615-7")
author = Shelf::Author.new("Ada")
book = Shelf::Book.new(isbn, "Notes", author)

catalog = Shelf::Catalog.new
catalog.add(book).add(Shelf::Book.new(Shelf::Isbn.new("9780131103627"), "The C Language", Shelf::Author.new("Brian")))

# No return keyword, no parentheses, method chaining.
titles = catalog.map(&:title).sort.map(&:upcase)
p titles
puts catalog.first
p catalog.count
p(catalog.any? { |b| b.title.start_with?("N") })
p catalog.to_a.empty?
p [3, 1, 2].sort!.first

Output:

["NOTES", "THE C LANGUAGE"]
Notes by Ada
2
true
false
1

The Catalog in the example never defines map, count, first or any?: it defines each and includes Enumerable, and the module supplies dozens of methods built on it. That pattern, one required method plus a mixin that adds the rest, recurs throughout the language and is covered in Modules and Mixins and Enumerable. The idioms themselves, with their trade-offs, are collected in Idioms, Style and Clean Code.

The principle of least surprise

The principle is often quoted as "Ruby behaves as you would expect". Matz has been explicit that this is his own sense of what is natural, shaped by Lisp, Smalltalk, Perl and others, so a programmer from a different background will find places where Ruby surprises them. Knowing where those places are is most of the work of learning Ruby.

# frozen_string_literal: true

# Things that surprise newcomers.
truthiness = ->(v) { v ? "truthy" : "falsy" }
p [0, "", [], {}].map(&truthiness)
p [nil, false].map(&truthiness)
p 7 / 2
p(-7 / 2)
p 7.fdiv(2)
p "ab" * 3
p [1, 2].join(", ")
p 0.1 + 0.2
a = "x"
b = a
p b.equal?(a)
p "x".equal?("x")
p :x.equal?(:x)
p 1.equal?(1)
nothing = nil
flag = false
puts [1, [2, [nothing, 3]]].inspect
puts nothing.to_s.empty?
puts "#{nothing}|#{flag}|#{[nothing]}"

Output:

["truthy", "truthy", "truthy", "truthy"]
["falsy", "falsy"]
3
-4
3.5
"ababab"
"1, 2"
0.30000000000000004
true
true
true
true
[1, [2, [nil, 3]]]
true
|false|[nil]

The usual sources of surprise:

What you may expect What Ruby does

0, "", [] and {} are false

Only nil and false are falsy. Everything else, including 0 and the empty string, is truthy.

7 / 2 is 3.5

Integer division of two integers is an integer, rounded toward negative infinity: 7 / 2 is 3 and -7 / 2 is -4. Use fdiv or a float operand for a float result.

0.1 + 0.2 is 0.3

Floats are IEEE 754 binary doubles, as in every mainstream language, so the result is 0.30000000000000004. Use Rational or BigDecimal for exact decimal arithmetic.

== compares identity (Java)

== compares values and is overridden by classes; equal? is identity; eql? is the equality used for hash keys (1.eql?(1.0) is false). See Equality, Hashing and Comparison.

Strings are immutable (Java, JavaScript, Python)

Strings are mutable objects. Assigning a string to another variable copies the reference, not the text, so << and upcase! change both. Frozen strings are opt-in (see below).

"x".equal?("x") is false

With # frozen_string_literal: true, identical literals in one file are the same frozen object, so it is true. Without the comment they are distinct.

puts [1, 2] prints [1, 2]

puts prints each array element on its own line; use p or inspect to see the structure. puts nil prints an empty line, and p nil prints nil.

"abc"[0] is a character code

It is the one-character string "a". ?a is a character literal, and "a".ord is the code point.

A name you never declared is an error

Assignment creates a local variable. An unassigned instance variable reads as nil (with no warning since Ruby 3.0); an unassigned local raises NameError.

Two settings change how much of this you see. The magic comment # frozen_string_literal: true makes string literals in that file frozen, which is why almost every file in this reference starts with it. Since Ruby 3.4, a literal in a file without the comment is "chilled": mutating it still works but emits a warning of the form literal string will be frozen in the future when deprecation warnings are enabled (-W:deprecated or $VERBOSE = true). In Ruby 4.0 this is still only a warning; the programs of a codebase should be migrated with the magic comment, as described in Program Structure and Lexical Elements and Strings.

More than one way to do it

Perl’s motto "there is more than one way to do it" describes Ruby too, though Ruby leans on a shared set of idioms more than Perl does. The language and its library are full of intentional overlap: map/collect, select/filter, inject/reduce, size/length, Array#first/take, unless/if !, and/&&, statement modifiers versus block forms, %w[] versus array literals, string interpolation versus format. The overlap exists to let code read naturally in context, and it is why a style guide matters more in Ruby than in languages with a single dominant idiom.

# frozen_string_literal: true

numbers = [1, 2, 3, 4]

p numbers.sum
p numbers.reduce(:+)
p(numbers.inject { |acc, n| acc + n })
total = 0
numbers.each { |n| total += n }
p total
p numbers.each_with_object([0]) { |n, acc| acc[0] += n }.first

# Several ways to say "the evens, squared".
p(numbers.select(&:even?).map { |n| n**2 })
p(numbers.filter_map { |n| n**2 if n.even? })
p(numbers.each_slice(2).map(&:last).map { it**2 })

Output:

10
10
10
10
10
[4, 16]
[4, 16]
[4, 16]

All the variants above are correct. A reasonable rule of thumb: pick the most specific method that names what you mean (sum rather than inject(:+), filter_map rather than select then map, select rather than an each that pushes onto an array), let RuboCop enforce the team’s choice, and do not mix synonyms such as collect and map in one codebase. See Style, Linting and Static Analysis.

Duck typing

Ruby never asks "what class are you?" when it can ask "can you do this?". A method that calls item.title works with any object that has a title method, whatever its ancestry. There are no interfaces to declare and no common superclass to share. When the object cannot answer, Ruby raises NoMethodError at the point of the call, and the exception carries the method name and the receiver.

# frozen_string_literal: true

require "shelf"

# Duck typing: anything with #title works, no common ancestor needed.
Pamphlet = Struct.new(:title)

def shout(item)
  item.title.upcase
end

book = Shelf::Book.new(Shelf::Isbn.new("9780306406157"), "Notes", Shelf::Author.new("Ada"))
p shout(book)
p shout(Pamphlet.new("flyer"))

begin
  shout(42)
rescue NoMethodError => e
  puts e.message
  p e.name
  p e.receiver
end

p [book, Pamphlet.new("x"), 42].select { |o| o.respond_to?(:title) }.size

# A role by convention: each + Enumerable.
class Rack
  include Enumerable

  def initialize(*items) = @items = items

  def each(&)
    return enum_for(:each) unless block_given?

    @items.each(&)
  end
end

s = Rack.new(3, 1, 2)
p s.sort
p s.min
p s.include?(2)
p s.each_slice(2).to_a

Output:

"NOTES"
"FLYER"
undefined method 'title' for an instance of Integer
:title
42
2
[1, 2, 3]
1
true
[[3, 1], [2]]

Ruby is dynamically typed (types are attached to values and checked when a method runs) and strongly typed (it does not silently convert between unrelated types; see the table later on this page). Idiomatic code therefore:

  • tests capabilities with respond_to? only when it must, and otherwise lets the call fail;

  • converts at the boundary with explicit conversion methods (to_s, Integer(), to_str), see Conversions and Coercion;

  • documents the expected "shape" of arguments in comments, in RBS signatures or in tests, rather than in declarations;

  • uses is_a?, case/when Class or pattern matching deliberately when behaviour really depends on the kind of object.

The cost is that mistakes surface at run time, which is why Ruby projects lean on tests (see Testing with Minitest) and, increasingly, on optional static checking (see Type Checking with RBS, TypeProf, Steep and Sorbet).

Open classes

A class or module block does not define a class so much as open it: if the constant already exists the body is evaluated inside the existing class and its definitions are added to it. This is how Ruby extends classes across files, how libraries add features, and how the language lets you patch behaviour.

# frozen_string_literal: true

require "shelf"

# Open classes: reopening a class adds to it.
module Shelf
  class Author
    def initials = name.split.map { |part| part[0] }.join
  end
end

p Shelf::Author.new("Ada Lovelace").initials
p Shelf::Author.instance_method(:initials).owner

Output:

"AL"
Shelf::Author

Core classes are open too:

# frozen_string_literal: true

# Even core classes are open. Convenient, and dangerous.
class Integer
  def double = self * 2
end

p 21.double
p Integer.instance_method(:double).owner
p Integer.instance_method(:double).source_location.last

Output:

42
Integer
5

Changing core classes globally ("monkey patching") is powerful and risky: the change is visible to every library in the process, two libraries can define the same method differently, and a later Ruby release may add a method with the same name and a different meaning. The contained alternative is a refinement, whose changes apply only in the file (or module body) that activated them with using:

# frozen_string_literal: true

# A refinement limits the change to a lexical scope.
module Shouting
  refine String do
    def shout = "#{upcase}!"
  end
end

begin
  "hi".shout
rescue NoMethodError => e
  puts e.message
end

using Shouting
p "hi".shout

Output:

undefined method 'shout' for an instance of String
"HI!"

Rules of thumb for open classes: reopen your own classes freely; reopen third-party classes rarely and document it; prefer a refinement, a subclass, a wrapper or a mixin to changing a core class; never redefine a core method with a different contract. The details are in Refinements and Monkey Patching.

What happens when Ruby runs your file

A frequent claim in introductions is that Ruby is "parsed and executed line by line". That describes a REPL such as IRB, which reads one expression at a time, but not what ruby file.rb does. Ruby reads the whole file, parses it into a syntax tree, compiles the tree to bytecode and only then starts executing. A syntax error anywhere in the file, even after code that would have run, stops the program before the first line executes.

flowchart LR S["Source file or -e string"] --> P["Prism parser"] P --> A["Syntax tree (AST)"] A --> C["Compiler"] C --> B["YARV bytecode (instruction sequence)"] B --> V["YARV interpreter"] V -.->|"hot methods"| J["YJIT (optional)"]
  • The parser is Prism, the default since Ruby 3.4 (it replaced the older parse.y parser). It is also available to programs as the prism default gem, which is how many tools read Ruby code.

  • The compiler produces YARV instruction sequences (RubyVM::InstructionSequence), which the virtual machine in MRI (the reference implementation, also called CRuby) interprets.

  • YJIT, the just-in-time compiler, is built into MRI since 3.1, is considered production-ready since 3.2, and is enabled with --yjit or RUBY_YJIT_ENABLE=1. ZJIT, a newer method-based JIT, appeared in Ruby 4.0 as an experimental option and is not used in the examples here. The older MJIT and RJIT were removed; the page YJIT, ZJIT and the VM covers them.

  • Requiring a file (require, load) repeats the same steps for that file at the moment of the call, which is why code can be loaded conditionally and why eval is possible.

A file with a syntax error shows the "whole file first" behaviour; the puts on line 1 never runs:

puts "this line never runs"
def broken(

Output (exit status 1, nothing printed before the error):

$ ruby broken.rb
broken.rb: --> broken.rb
Unmatched `(', missing `)' ?
Unmatched keyword, missing `end' ?
> 1  puts "this line never runs"
> 2  def broken(
broken.rb:2: syntax errors found (SyntaxError)
  1 | puts "this line never runs"
> 2 | def broken(
    | ^~~ expected an `end` to close the `def` statement
    |            ^ unexpected end-of-input, assuming it is closing the parent top level context
    |            ^ unexpected end-of-input; expected a `)` to close the parameters

Source: Prism

The parser and compiler are visible from Ruby itself:

# frozen_string_literal: true

require "prism"

source = <<~RUBY
  puts "this line never runs"
  def broken(
RUBY

result = Prism.parse(source)
p result.success?
puts result.errors.map(&:message).first(2)

ok = Prism.parse("a = 1 + 2")
p ok.success?
p ok.value.statements.body.first.class
p ok.value.statements.body.first.value.class

iseq = RubyVM::InstructionSequence.compile("a = 1 + 2")
puts iseq.disasm.lines.grep(/putobject|opt_plus|setlocal|leave|putself/).map(&:strip)

Output:

false
unexpected end-of-input; expected a `)` to close the parameters
unexpected end-of-input, assuming it is closing the parent top level context
true
Prism::LocalVariableWriteNode
Prism::CallNode
0000 putobject_INT2FIX_1_                                             (   1)[Li]
0001 putobject                              2
0003 opt_plus                               <calldata!mid:+, argc:1, ARGS_SIMPLE>[CcCr]
0006 setlocal_WC_0                          a@0
0008 leave

The exact error text and the instruction names are implementation details of MRI and change between releases. What stays stable is the model: parse the file, compile it, run it. ruby -c file.rb (and ruby -wc, which also reports warnings) performs only the parse step and prints Syntax OK; see Ruby Command Line and Environment.

Coming from other languages

The official site collects short guides for readers who know C and C++, Java, Perl, PHP or Python. The sections below cover the same ground, extended to JavaScript, C# and Go and focused on the places where a habit from another language produces wrong Ruby. They are not a substitute for the topic pages, which they link to.

Concept map

Concept In other languages In Ruby

Interface

Java and C# interface, Go interface, PHP interface, TypeScript interface

No keyword. Duck typing for call sites, and a module mixed in with include to share behaviour; "interfaces" are documented by convention, tests or RBS.

Abstract class

abstract (Java, C#, PHP)

A class whose methods raise NotImplementedError, or a module the including class completes. Nothing stops instantiation.

Generics

List<T> (Java, C#), generics (Go 1.18, TypeScript), templates (C++)

None at run time. Containers hold any object. RBS adds generic signatures, Array[Book], for tools such as Steep.

Exceptions

try/catch/finally, throw

begin/rescue/else/ensure and raise. There are no checked exceptions. catch/throw exist but are for non-local exit, not errors.

Lambdas and closures

Java lambdas, C# delegates, JavaScript arrow functions, Python lambda, Go function literals

Blocks (do ... end, { }) passed to methods, proc, and lambda or the stabby ->. All are closures.

Packages and imports

Java package/import, Python import, C# namespace/using, PHP namespace/use, JavaScript modules

module for namespacing and require (or autoload) to load a file. Distribution is by gem.

Annotations and decorators

Java @Override, C# attributes, Python decorators, TypeScript decorators

No annotations. Class-level method calls ("class macros") such as attr_accessor, private, module_function, private_constant; magic comments; tools read YARD tags in comments.

Properties and fields

C# properties, Java getters and setters, PHP typed properties

Instance variables (@x) are always private; attr_reader, attr_writer and attr_accessor generate methods.

Constructors

new Foo() with overloaded constructors

Foo.new(...) calls initialize. No overloading: use default, keyword or splat arguments, or alternative self. class methods.

Records and value types

Java record, C# record, Python dataclass, Go struct

Struct and, since Ruby 3.2, immutable Data.

Enums

enum

No keyword. Symbols (:lent), frozen constants and small classes; see Symbols.

Switch

switch on a value

case/when (uses ===, so ranges, classes, regexps and lambdas match) and case/in pattern matching since 3.0.

Null

null, None, undefined, nil

nil, a real object. &. for safe navigation. No undefined.

Iteration

for (int i...), for x in, foreach

each, times, upto, each_with_index, map; loop do for open-ended loops. for exists but is rarely used.

Increment

i++

i += 1. There is no ++ or --.

Async

async/await, goroutines, promises

Threads (GVL-limited), Fibers with a scheduler, Ractors; no async keyword.

Package manager

Maven, npm, pip, NuGet, Composer, Go modules

RubyGems (gem) and Bundler (Gemfile, bundle).

Test frameworks

JUnit, pytest, Jest, PHPUnit, testing

Minitest (bundled) and RSpec.

Truthiness and equality across languages

Value Ruby Python JavaScript PHP Java, C#, Go

0

truthy

falsy

falsy

falsy

not a boolean (compile error)

""

truthy

falsy

falsy

falsy

not a boolean

[]

truthy

falsy

truthy

falsy

not a boolean

{}

truthy

falsy

truthy

falsy (empty array)

not a boolean

nil, None, null

falsy

falsy

falsy

falsy

not a boolean

false

falsy

falsy

falsy

falsy

false

"0"

truthy

truthy

truthy

falsy

not a boolean

Because only nil and false are falsy, if count is true for zero. Test what you mean: count.zero?, list.empty?, name.nil?. The ||= operator therefore only fills in missing (nil or false) values, which is what most memoisation needs but is a trap when false is a legitimate stored value.

A method definition does not close over locals

def starts a new local scope. A method cannot see the local variables of the surrounding code, while blocks and lambdas can. This differs from JavaScript and Python, where a nested function closes over the enclosing scope, and it is the reason a script’s top-level variables cannot be used inside its methods.

# frozen_string_literal: true

# def opens a new scope; blocks and lambdas close over the surrounding one.
limit = 3

def uses_outer
  limit
end

begin
  uses_outer
rescue NameError => e
  puts e.message
end

p([1, 2, 3, 4].select { |n| n < limit })
p(-> { limit }.call)
p defined?(limit)
p defined?(undefined_thing)
p defined?(puts)
p defined?(String)
p defined?(@name)

# No declarations: assignment creates the variable, nil is the default for instance variables.
class Tracker
  def peek = @seen
end

p Tracker.new.peek

# for does not create a scope; each does.
for i in 1..2 do end
p i
[1, 2].each { |j| j }
p defined?(j)

# self is always the receiver; there is no rebinding of this.
p self
p self.class

Output:

undefined local variable or method 'limit' for main
[1, 2]
3
"local-variable"
nil
"method"
"constant"
nil
nil
2
nil
main
Object

The for loop in that example is the one construct that does not create a scope: its variable remains after the loop. That surprising behaviour is a reason the community prefers each. Scope rules in full are in Variables, Constants and Scope.

Strong, dynamic typing

Ruby will not turn a number into a string, or a string into a number, to make an operator work. This sits between JavaScript and PHP (which coerce freely) and Java, C# and Go (which refuse at compile time).

# frozen_string_literal: true

# Ruby is dynamically but strongly typed: no implicit conversions between unrelated types.
prefix = "total: "
count = 3
text = "3"

begin
  prefix + count
rescue TypeError => e
  puts e.message
end

begin
  count + text
rescue TypeError => e
  puts e.message
end

begin
  nil + 1
rescue NoMethodError => e
  puts e.message
end

p "total: #{count}"
p text.to_i + count
p "abc".to_i
p Integer("42")
p "1" == 1
p [1, 2] + [3]
p nil&.length
p nil.to_a
p({ a: 1 }[:b])
p({ a: 1 }.fetch(:a))
begin
  { a: 1 }.fetch(:b)
rescue KeyError => e
  puts e.message
end
p [1, 2][5]
p 2**100
p((2**100) / (2**98))
p 10.0 / 3
p 10 % 3
p(-10 % 3)

Output:

no implicit conversion of Integer into String
String can't be coerced into Integer
undefined method '+' for nil
"total: 3"
6
0
42
false
[1, 2, 3]
nil
[]
nil
1
key not found: :b
nil
1267650600228229401496703205376
4
3.3333333333333335
1
2

The same example shows three more habits: "abc".to_i returns 0 where Integer("abc") raises (use the strict Integer() at trust boundaries); Hash#[] and Array#[] return nil for a missing key or index while fetch raises KeyError; and integers have arbitrary precision (2**100 does not overflow), so there are no int/long choices and no wrap-around. Modulo follows the sign of the divisor, as in Python and unlike C, Java and JavaScript.

From Java and C#

  • Classes and files. There is no one-class-per-file rule, no public class naming the file and no package-private access. A file can define any number of classes and modules; by convention (and by the autoloaders) a file is named after its constant, book.rb for Book.

  • Interfaces become duck typing plus mixins. A method that needs title takes anything with title. Shared behaviour goes into a module included in several classes (below). Java default methods and C# extension methods are both approximated by include, and by refinements.

  • No overloading. Defining a method twice replaces it (with a warning under -w). Use optional and keyword arguments, *args, or inspect the argument.

  • Visibility is about the receiver, not the class. private methods can be called only without an explicit receiver (and self. receivers for setters and, since Ruby 2.7, any method); protected allows calls from instances of the same class; instance variables are always private to the object. There is no final, but freeze and private_constant provide the spirit. See Visibility and Encapsulation.

  • Single inheritance plus modules. A class has one superclass and any number of mixed-in modules; the method lookup order is the ancestor chain, see Method Lookup and super.

  • == is not identity. a == b calls a method that classes override. Compare identity with equal?.

  • Properties. C# properties and Java getters and setters are attr_reader, attr_writer and attr_accessor, or hand-written methods named name and name=. Assigning with obj.name = value is a method call.

  • LINQ and streams. Enumerable, and Enumerator::Lazy for lazy pipelines, with map, select, group_by, sort_by, sum, each_slice, tally, zip and the rest in Enumerable.

  • using/try-with-resources. Methods that take a block manage the resource and close it afterwards: File.open(path) { |f| ... }, Mutex#synchronize { ... }.

  • Nullable references. There is no compile-time null check. &. (safe navigation), Array#compact and Hash#fetch with a default cover the common cases.

  • Annotations, @Override. Nothing marks an override; the keyword super calls the parent implementation, and RuboCop or RBS can check intent.

  • Static members. class << self, def self.name and module_function take the place of static; class-level state lives in class instance variables. See Singleton Classes and Class-Level Behavior.

  • Generics, async, partial class. See the sections below; partial class is simply reopening a class.

Java and C# readers should see the Java Reference and C# Reference on this site for the other side of each comparison.

Interfaces and mixins

Where Java declares an interface and several classes implement it, Ruby writes a module with the shared behaviour and mixes it into each class. A module that depends on a method the class must supply (label here) is the Ruby analogue of an abstract method, but the check happens at call time:

# frozen_string_literal: true

# The Java "interface" becomes a convention plus a mixin.
module Describable
  def describe = "#{self.class.name.downcase}: #{label}"
end

Magazine = Struct.new(:label) { include Describable }
Poster = Struct.new(:label) { include Describable }

p Magazine.new("monthly").describe
p Poster.new("A2").describe
p Magazine.include?(Describable)
p Magazine.new("x").is_a?(Describable)
p Describable.instance_methods

# The module needs #label, but nothing checks that until it is called.
Broken = Class.new { include Describable }

begin
  Broken.new.describe
rescue NameError => e
  puts e.message
  p e.class
end

# Comparable gives <, <=, ==, >, >= and between? from a single <=>.
Version = Struct.new(:major, :minor) do
  include Comparable

  def <=>(other) = to_a <=> other.to_a
end

p Version.new(1, 2) < Version.new(1, 10)
p Version.new(1, 2).between?(Version.new(1, 0), Version.new(2, 0))
p [Version.new(2, 0), Version.new(1, 5)].min

Output:

"magazine: monthly"
"poster: A2"
true
true
[:describe]
undefined local variable or method 'label' for an instance of Broken
NameError
true
true
#<struct Version major=1, minor=5>

Comparable and Enumerable are the two mixins worth learning first: define <=> and you get the comparison operators, between?, clamp and sort/min/max support; define each and you get the whole collection API.

Exceptions

try/catch/finally becomes begin/rescue/ensure, with an optional else that runs when nothing was raised. raise Class, "message" throws, a bare raise inside rescue re-raises, and retry re-runs the begin block. A def body and a do ... end block (since Ruby 2.5) can carry rescue and ensure clauses directly, without an enclosing begin.

# frozen_string_literal: true

require "shelf"

# try/catch/finally becomes begin/rescue/ensure (and an optional else).
def parse_isbn(text)
  Shelf::Isbn.new(text)
rescue ArgumentError => e
  warn "rejected: #{e.message}"
  nil
ensure
  puts "checked #{text.inspect}"
end

p parse_isbn("9780306406157")
p parse_isbn("123")

# A custom exception hierarchy, and retry.
class NotFound < Shelf::Error; end

attempts = 0
begin
  attempts += 1
  raise NotFound, "no such book" if attempts < 3

  puts "found after #{attempts} attempts"
rescue NotFound
  retry
end

# raise with no argument re-raises; exceptions carry a cause.
begin
  begin
    Integer("abc")
  rescue ArgumentError
    raise NotFound, "lookup failed"
  end
rescue Shelf::Error => e
  p e.class
  p e.cause.class
  puts e.cause.message
end

# Many conversions take exception: false instead of raising.
p Integer("x", exception: false)
p Float("1.5", exception: false)

Output:

checked "9780306406157"
#<data Shelf::Isbn digits="9780306406157">
rejected: invalid ISBN: 123
checked "123"
nil
found after 3 attempts
NotFound
ArgumentError
invalid value for Integer(): "abc"
nil
1.5

Differences that catch newcomers:

  • A bare rescue handles StandardError and its subclasses only; never rescue Exception, which also traps SystemExit and Interrupt.

  • ensure runs on every exit, including return, break and throw; an explicit return inside ensure swallows an exception in flight.

  • The value of a begin block with an else is the value of the else branch, not of the body.

  • Kernel#catch and Kernel#throw are an unrelated, lightweight non-local-exit mechanism; they are not exceptions.

  • Ruby has no checked exceptions and no throws clause; methods are not annotated.

Lambdas and closures

Java lambdas, C# delegates and JavaScript arrow functions correspond to Ruby’s three closure forms. A block is an anonymous chunk of code attached to a method call; a proc is a block turned into an object; a lambda is a proc with method-like argument checking and return. They all capture the variables around them.

# frozen_string_literal: true

# Blocks, procs and lambdas: closures for lambdas / function values.
double = ->(n) { n * 2 }
p double.call(4)
p double[4]
p double.lambda?
p [1, 2, 3].map(&double)
p [1, 2, 3].map(&:to_s)

compose = double >> ->(n) { n + 1 }
p compose.call(5)

counter = 0
increment = -> { counter += 1 }
3.times { increment.call }
p counter

def adder(step) = ->(n) { n + step }
p adder(10).call(1)

def each_twice(items)
  return to_enum(__method__, items) unless block_given?

  items.each { |i| 2.times { yield i } }
end

each_twice([1, 2]) { |i| print i, " " }
puts
p each_twice([1, 2]).to_a

pr = proc { |a, b| [a, b] }
p pr.call(1)
p pr.call(1, 2, 3)
p pr.call([4, 5])
begin
  double.call
rescue ArgumentError => e
  puts e.message
end
p method(:puts).to_proc.lambda?

Output:

8
8
true
[2, 4, 6]
["1", "2", "3"]
11
3
11
1 1 2 2
[1, 1, 2, 2]
[1, nil]
[1, 2]
[4, 5]
wrong number of arguments (given 0, expected 1)
true

Notes for readers from other languages:

  • Calling a method needs no parentheses, but referring to a method without calling it is not possible: method(:puts) returns a Method object, and &method(:name) or &:name pass it where a block is expected. There is no bare function reference as in JavaScript or Python.

  • A lambda checks its argument count (wrong number of arguments (given 0, expected 1)), a proc does not (missing values are nil, extra values are dropped, a single array argument is destructured).

  • return in a lambda returns from the lambda; return in a proc or block returns from the enclosing method.

  • yield calls the block given to the current method, and block_given? tests whether there is one. A method that returns an enumerator when called without a block is a common Ruby idiom (return to_enum(__method__, items) unless `block_given?).

  • The compose operators >> and << combine callables. Currying is Proc#curry.

Annotations become class macros

Ruby has no annotation syntax. The same needs (declare an accessor, mark a method private, register a callback, deprecate a name) are met by calling methods on the class from inside its body, which run when the class is defined. attr_accessor, private, include, private_constant and define_method are all ordinary methods.

# frozen_string_literal: true

# Annotations become class-level method calls ("class macros").
class Settings
  attr_accessor :host
  attr_reader :port

  def initialize
    @host = "localhost"
    @port = 8080
  end

  DEFAULTS = { host: "localhost" }.freeze
  private_constant :DEFAULTS

  def self.deprecated_alias(old, replacement)
    define_method(old) do |*args, &blk|
      warn "#{old} is deprecated, use #{replacement}"
      public_send(replacement, *args, &blk)
    end
  end

  deprecated_alias :legacy_host, :host
end

s = Settings.new
p s.legacy_host
p Settings.instance_methods(false).sort
p Settings.private_instance_methods(false)
begin
  Settings::DEFAULTS
rescue NameError => e
  puts e.message
end
p s.frozen?
p s.freeze.frozen?
begin
  s.host = "x"
rescue FrozenError => e
  p e.class
end

Output:

legacy_host is deprecated, use host
"localhost"
[:host, :host=, :legacy_host, :port]
[:initialize]
private constant Settings::DEFAULTS referenced
false
true
FrozenError

This is the foundation of Ruby DSLs, covered in Dynamic Dispatch and Method Definition and Hooks, eval and DSLs. The magic comments at the top of a file (frozen_string_literal, shareable_constant_value) are the closest thing to file-level annotations, and documentation comments with YARD tags (@param, @return) are read by tools, not by the interpreter; see Documentation with RDoc and YARD.

Generics become duck typing plus RBS

An Array holds anything, a method takes anything, and the only "type error" is a NoMethodError or TypeError at the moment a value is misused:

# frozen_string_literal: true

# Without generics: containers hold anything, types are checked at use.
mixed = [1, "two", :three, nil, 4.0, [5]]
p mixed.map(&:class)
p mixed.grep(Numeric)

def total(items)
  items.sum
end

p total([1, 2, 3])
p total([1.5, 2])
begin
  total(%w[a b])
rescue TypeError => e
  puts e.message
end
p total([[1], [2]].map(&:first))
p %w[a b].sum("")

Output:

[Integer, String, Symbol, NilClass, Float, Array]
[1, 4.0]
6
3.5
String can't be coerced into Integer
3
"ab"

Static types are optional and live outside the program. Since Ruby 3.0, RBS is the standard language for describing types in separate .rbs files; tools such as TypeProf (inference), Steep (checking) and Sorbet (a separate system with its own inline syntax) consume them. RBS has generics, so Array[Book] and Enumerable[Book] are expressible:

module Shelf
  class Catalog
    include Enumerable[Book]

    def initialize: (?store: Store::Memory) -> void
    def add: (Book book) -> self
    def each: () { (Book) -> void } -> void
            | () -> Enumerator[Book, void]
  end
end

Packages become gems and modules

Java’s package plays two roles that Ruby separates. Namespacing is done by module (or class) nesting: Shelf::Store::Memory. Distribution is done by gems, versioned archives with a .gemspec, installed by RubyGems and resolved by Bundler from a Gemfile. Loading is separate again: nothing is imported automatically, a file must be `require`d (or autoloaded) before its constants exist, and the file layout carries no meaning to the interpreter.

# frozen_string_literal: true

require "shelf"

# Packages become gems (distribution) and modules (namespacing); files are not namespaces.
p Shelf.constants.sort.first(5)
p Shelf::VERSION
p($LOADED_FEATURES.grep(/shelf\.rb/).map { File.basename(it) })
p require("shelf")
p defined?(Shelf::Store::Memory)
p Shelf::Store::Memory.name
module Shelf
  module Store
    p Memory
  end
end
p Set.new([1, 2, 3]).size

Output:

[:Author, :Book, :Catalog, :Config, :Error]
"0.1.0"
["shelf.rb"]
false
"constant"
"Shelf::Store::Memory"
Shelf::Store::Memory
3

Note that Set needs no require: it has been autoloaded since Ruby 3.2 and is a core class in Ruby 4.0. Key points: require returns false the second time (a file loads once), require_relative resolves from the current file, and autoload/Zeitwerk load constants on demand. See Code Loading: require and autoload and RubyGems and Bundler.

Fields, constructors and value types

A class declares no fields. Instance variables appear when first assigned (usually in initialize), are visible only inside the object’s methods, and are exposed through methods. Where Java uses a record or Python a dataclass, Ruby has Struct (mutable, positional by default) and Data (immutable, since Ruby 3.2):

# frozen_string_literal: true

# Java-style class vs Ruby: fields, accessors, constructors.
class Reader
  attr_reader :name
  attr_accessor :email

  def initialize(name, email: nil)
    @name = name
    @email = email
  end

  def to_s = "#{name} <#{email || 'n/a'}>"
end

r = Reader.new("Ada", email: "ada@example.com")
puts r
r.email = nil
puts r
p r.instance_variables
p r.respond_to?(:name=)

Pair = Struct.new(:left, :right)
Point = Data.define(:x, :y)
p Pair.new(1, 2)
p Point.new(x: 1, y: 2)
p Point.new(1, 2).with(y: 5)
first = Point.new(1, 2)
p first == Point.new(1, 2)
p Reader.new("a").eql?(Reader.new("a"))

Output:

Ada <ada@example.com>
Ada <n/a>
[:@name, :@email]
false
#<struct Pair left=1, right=2>
#<data Point x=1, y=2>
#<data Point x=1, y=5>
true
false

Two ordinary classes with equal fields are not equal unless the class defines == (and eql? and hash if it is used as a hash key); Struct and Data define all three from their members. See Struct, Data and Value Objects and Classes and Objects.

From Python

Ruby and Python occupy similar territory (dynamic, strongly typed, garbage collected, interactive) and differ mostly in syntax and in how code is organised.

  • Blocks, not indentation. Blocks end with end; indentation is style, not syntax.

  • No list comprehensions. [x * 2 for x in xs if x > 1] is xs.select { |x| x > 1 }.map { |x| x * 2 } or filter_map.

  • self is implicit. It is not a parameter; call self.method only when needed (setters, private rules). Methods are called with a receiver, and instance variables carry an @.

  • Methods are not attributes. obj.name always calls a method; there is no attribute lookup separate from method lookup, so there is no property decorator, only attr_* and plain methods.

  • Mixins instead of multiple inheritance. A class has one parent and any number of modules.

  • Functions are not values. A bare name calls the method; use method(:name) for a reference.

  • Strings are mutable and str[0] is a one-character string. Symbols (:name) are interned identifiers, often used for hash keys and options.

  • Dictionaries are Hash, ordered by insertion since Ruby 1.9, with { key: value } as the common literal.

  • No tuples. Use arrays, Struct or Data; multiple return values are an array destructured by multiple assignment (a, b = pair).

  • Truthiness. Empty collections, 0 and "" are truthy; see the table above.

  • Integer division floors like Python’s //, but spelled / between integers.

  • Errors. raise/rescue instead of raise/except; ensure instead of finally; retry has no equivalent.

  • Standard library. with open(...) is File.open(...) { ... }; import is require; pip is gem and Bundler; virtual environments are replaced by a Gemfile and bundle exec plus a version manager; unittest/pytest are Minitest/RSpec.

  • Concurrency. Like CPython, MRI has a global VM lock for Ruby threads; Ractors and Fibers are the other tools.

See the Python Reference for the other side of the comparison.

From JavaScript and TypeScript

  • const is not Ruby’s constant. A Ruby constant (capitalised name) can be reassigned, with a warning (already initialized constant); it is a naming convention for shared values, and the value stays mutable unless it is frozen.

  • this is self, and it is always the receiver of the current method. There is no rebinding by call site; instance_eval and instance_exec change it explicitly.

  • No undefined, no hoisting, no var. Variables come into existence on assignment, and a read before assignment of a local raises NameError.

  • Object literals are hashes. { name: "Ada" } is a Hash with symbol keys, accessed as h[:name], not h.name. Struct, Data or a class gives dot access.

  • Arrow functions are blocks, proc or -> lambdas. Ruby has no function-valued property syntax.

  • Strings convert explicitly. "1" + 1 raises TypeError; "1" == 1 is false; interpolation ("#{n}") replaces template literals.

  • == and === differ from JS. Ruby == does no coercion; Ruby === is case equality used by case/when (Integer === 3 is true) and not strict equality.

  • async/await do not exist. Concurrency uses threads, fibers (with a scheduler for non-blocking I/O) and Ractors; an event loop comes from a gem.

  • Destructuring and spread are multiple assignment (a, (b, c) = ...), the splat operators * and **, and pattern matching (case/in).

  • npm/package.json/node_modules correspond to gem, Gemfile and Bundler’s vendored or system gems.

  • Optional chaining ?. is &.; nullish coalescing ?? has no direct equivalent: || treats false as missing, Hash#fetch and Object#then cover the rest.

See the JavaScript Development guide.

From PHP

  • No sigil on variables. $name is name; sigils in Ruby (@, @@, $) denote instance, class-variable and global scope, not type. Globals are rare.

  • -> and :: become . for method calls; :: is only for constants and namespacing. new Foo is Foo.new.

  • Arrays split in two. PHP’s single ordered map is Array (positional) plus Hash (keyed, insertion-ordered).

  • null, isset, empty become nil, nil?, empty? and defined?. There is no "falsy empty array".

  • Namespaces and use become nested module`s; `require is explicit, with Bundler and autoloading (Zeitwerk) replacing Composer’s classmap/PSR-4.

  • Exceptions work as in PHP, with rescue clauses instead of catch; all exceptions inherit from Exception.

  • Type juggling is gone. "10" == 10 is false, "abc" + 1 raises.

  • No request/response globals. Web frameworks (Rails, Sinatra, Rack applications) provide them; plain Ruby has no built-in web page model.

See the PHP Reference.

From Go

  • Interfaces are implicit in both languages, but Go checks them at compile time and Ruby only when a message is sent. A Go interface becomes a convention, a module or an RBS interface.

  • Errors are exceptions, not return values: there is no if err != nil ladder, and ensure replaces defer.

  • Goroutines and channels map to threads, Thread::Queue, fibers and Ractors. Ruby threads are limited by the global VM lock for Ruby code, so CPU-bound parallelism needs processes or Ractors.

  • Structs and methods become classes: methods live inside the class body, with no receiver declaration. Struct and Data give struct-like values.

  • Composition by embedding is composition by delegation or by include; there is no struct embedding.

  • Zero values do not exist: an uninitialised instance variable is nil.

  • go mod, gofmt, go vet, go test correspond to Bundler, RuboCop (not part of Ruby itself), ruby -wc and Minitest.

  • A single static binary has no Ruby equivalent: a Ruby program needs an interpreter and its gems on the target machine, or a packaged distribution.

The Go reference on this site covers the other side of the comparison.

From C and C++

  • No pointers, no manual memory. Objects are references and the garbage collector reclaims them; free and destructors are replaced by ensure and block-based resource management.

  • Integers do not overflow (they become big integers), floats are doubles, and there are no typedefs or casts, only conversion methods.

  • No headers, no preprocessor. require loads a file at run time; macros are replaced by methods and blocks.

  • Native code is reached through C extensions, FFI (the ffi gem, or Fiddle, a bundled gem in Ruby 4.0) or WebAssembly; see C Extensions, FFI and WebAssembly.

  • Templates are replaced by duck typing: any object that responds to the used messages works.

See the C Reference and C++ Reference.

From Perl

Ruby inherited a lot from Perl: regular expression literals and =~, $~-style match globals, heredocs, __END__, the -n, -p and -e command-line switches and the unless/until modifiers. The differences: variables have no type sigils, there is no context (scalar versus list); everything is an object; use strict-like behaviour is built in; references are implicit; and modules are classes. See Regular Expressions and Ruby Command Line and Environment.

Other implementations in one paragraph

Everything above describes MRI (CRuby), the reference implementation. Alternatives share the language but differ in runtime. JRuby runs on the JVM and integrates with Java libraries, TruffleRuby runs on GraalVM, and mruby is a small embeddable implementation for constrained environments. Library authors check which implementations they support; most application code runs on MRI.

Where the books are outdated

Older books and tutorials teach Ruby 1.8 to 2.x. Most of what they say about philosophy remains right; the details below have changed, and the concept pages in this reference have the current rules.

Older statement Current state (Ruby 4.0)

Ruby is "parsed and executed line by line"

The whole file is parsed by Prism, compiled to YARV bytecode and then run. Only IRB evaluates line by line. Syntax errors stop the program before it starts.

MRI is a slow interpreter with a global lock and no JIT

MRI compiles to bytecode, has YJIT (production-ready since 3.2), and experimental ZJIT in 4.0 (RJIT and MJIT are gone); the VM lock still serialises Ruby threads, and Ractors provide parallelism.

OpenStruct, Struct and hashes are the value-object options

Data (3.2) is the immutable value object; OpenStruct was promoted from a default gem to a bundled gem in Ruby 4.0 and is best avoided.

Interfaces and generics cannot be expressed

RBS (3.0) describes both; Steep and TypeProf use it; Sorbet is an alternative.

require "set" is needed

Set is autoloaded since Ruby 3.2 and a core class in Ruby 4.0.

String literals are mutable

Literals are chilled since 3.4: mutation warns under -W:deprecated; # frozen_string_literal: true is the explicit opt-in.

A trailing hash is accepted as keyword arguments

Ruby 3.0 separated positional and keyword arguments; a trailing hash is no longer converted automatically.

Pattern matching does not exist

case/in (2.7, stable in 3.0) and one-line in/=> forms.

Numbered block parameters _1 are the shortcut

it (3.4) is the idiom for a single implicit parameter; _1 still works.

Kernel#open with a leading pipe, DateTime, Fixnum/Bignum

Process creation by Kernel#open with a leading pipe character was removed in Ruby 4.0; DateTime is discouraged in favour of Time; Fixnum and Bignum were unified into Integer in Ruby 2.4.

Source: Ruby 4.0 NEWS

The full list of changes by version is in What’s New and Migrating Older Code and Versions, Releases and Compatibility.

Where to go next