Classes and Objects

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, checked on 2026-10-18). Ruby is an object-oriented language in the strictest sense: every value is an object, every object is an instance of a class, and a class is itself an object (an instance of Class). This page covers how classes are defined and instantiated, what new really does, how instance state is stored and exposed, how objects print, what reopening and inheriting a class mean, how to ask an object what it is, the three roots of the hierarchy (BasicObject, Object and Kernel), and how to design small, collaborating classes instead of large ones.

It builds on Methods and Parameters (every method in a class body is defined with def), Variables, Constants and Scope (instance variables, class variables and constants) and Exceptions and Error Handling. The pages that follow go deeper on each topic that is only introduced here: Modules and Mixins, Singleton Classes and Class-Level Behavior, Method Lookup and super, Visibility and Encapsulation, Refinements and Monkey Patching, Equality, Hashing and Comparison, Object Lifecycle, Freezing and Copying and Struct, Data and Value Objects. For how other languages model the same ideas, see Java, Python and Kotlin.

Every example was syntax-checked with ruby -wc, run with ruby -w on Ruby 4.0.7 and linted with RuboCop 1.92 using the configuration of the running shelf gem (double-quoted strings, NewCops: enable, Style/Documentation off, and Style/OneClassPerFile off because several small classes share one example file). The output shown is the real output; memory addresses such as 0x000000012035b6e8 differ from run to run. The examples that need the gem are run with ruby -Ilib, so require "shelf" finds it (see the shelf gem source).

Objects and classes in one page

Term Meaning

Object

A value with an identity, some state (instance variables) and behaviour (methods found through its class). nil, true, integers and classes are objects too.

Class

A blueprint and a factory: the object that holds the instance methods shared by its instances and that creates them with new. Every class is an instance of Class.

Instance

An object created by a class. Its class is available as obj.class.

Instance variable

A value stored in one object, named with a leading @. It exists once it is assigned, not when it is declared.

Method

Code stored in a class (or module) and run with a receiver. def inside a class body defines an instance method.

Constant

The name a class is stored under (Member). A class statement creates the constant if it does not exist yet.

Superclass

The class a class inherits from. Every class has exactly one, except BasicObject, which has none.

Singleton class

A hidden per-object class that holds methods defined for that single object. Class methods live in the singleton class of the class. See Singleton Classes and Class-Level Behavior.

The Ruby reference says that the keyword defines a class "in the current scope", and that the name must be a constant: conventionally CamelCase, with each word capitalised and no underscores (LibraryCard, not library_card). A class body is ordinary executable code: it runs top to bottom when Ruby reaches it, with self set to the class, and the value of the whole class ... end expression is the value of the last expression in the body (like a method).

# frozen_string_literal: true

require "shelf"

# A class is an object, created by the class keyword and stored in a constant.
class Member
  attr_reader :name, :loans

  def initialize(name)
    @name = name
    @loans = []
  end

  def borrow(book)
    @loans << book
    self
  end

  def to_s = "#{name} (#{loans.size} loans)"
end

author = Shelf::Author.new("Ursula K. Le Guin")
isbn = Shelf::Isbn.new("978-0-306-40615-7")
book = Shelf::Book.new(isbn:, title: "The Dispossessed", author:)

ada = Member.new("Ada").borrow(book)
puts ada
p ada.class
p Member.class
p Member.name
p ada.instance_variables
p ada.is_a?(Member)
# A class definition is an expression: its value is the last expression.
result =
  class Probe
    42
  end
p result
Ada (1 loans)
Member
Class
"Member"
[:@name, :@loans]
true
42

Member.class is Class, and Member.name is the string "Member": a class knows the constant it was first assigned to (see Anonymous classes and Class.new). ada.instance_variables lists the two variables the constructor assigned, in the order they were first assigned.

The rest of the page is organised around the life of an object: how a class is shaped (reading and writing state, printing), how objects are created, and how classes relate to each other.

Creating objects

new, allocate and initialize

Member.new("Ada") is not syntax: new is an ordinary public method of Class (the instance method Class#new). It does two things, which can be written by hand:

  1. allocate asks the class for a fresh, empty object: right class, no instance variables, no initialize call.

  2. It calls the private instance method initialize on that object, passing on every argument, keyword and block it was given. The value initialize returns is ignored: new always returns the new object.

Because initialize is private, it cannot be called from outside (Member.new("Ada").initialize("Bob") is a NoMethodError); it runs only through new (or send, as the helper below does). BasicObject defines an empty initialize that accepts no arguments, which is why Object.new(1) raises ArgumentError. A class without its own initialize therefore takes no arguments at all.

# frozen_string_literal: true

class Member
  attr_reader :name

  def initialize(name)
    @name = name
    @name.length # the value of initialize is discarded
  end
end

# Class#new is allocate followed by a call to the private initialize method.
def build(klass, ...)
  object = klass.allocate
  object.__send__(:initialize, ...)
  object
end

p Member.allocate
p build(Member, "Ada")
p Member.new("Grace")
p Member.method(:new).owner
p Member.instance_method(:initialize).owner
p Member.private_instance_methods(false)

begin
  Member.new("Ada").initialize("Bob")
rescue NoMethodError => e
  puts e.message
end

begin
  Member.new
rescue ArgumentError => e
  puts e.message
end

begin
  Integer.new
rescue NoMethodError => e
  puts e.message
end

begin
  Integer.allocate
rescue TypeError => e
  puts e.message
end
#<Member:0x000000012467b8b0>
#<Member:0x000000012467b6d0 @name="Ada">
#<Member:0x000000012467b590 @name="Grace">
Class
Member
[:initialize]
private method 'initialize' called for an instance of Member
wrong number of arguments (given 0, expected 1)
undefined method 'new' for class Integer
allocator undefined for Integer

Member.allocate shows an object with no @name: initialize never ran. Reading an instance variable that was never assigned returns nil (see Instance variables). Two more details of this output are worth knowing:

  • Member.private_instance_methods(false) lists [:initialize]: Ruby makes a method named initialize (and initialize_copy, initialize_clone, initialize_dup and respond_to_missing?) private automatically, whatever its position relative to a private call.

  • The core classes that cannot have instances created the normal way undefine new on their singleton (Integer.new is undefined method 'new' for class Integer) and allocate raises TypeError (allocator undefined for Integer). Integers, Symbol, nil, true and false exist only as literals or results of operations. Class.allocate.new (an uninitialised class) raises TypeError: can't instantiate uninitialized class.

The wrong-arity message wrong number of arguments (given 0, expected 1) is ArgumentError, raised before the body of initialize runs. For the parameter kinds initialize can declare (required, optional, keyword, rest, block) see Methods and Parameters; the constructor is a method like any other.

Constructors without I/O

A constructor should leave the object ready to use and nothing else: assign instance variables, validate the arguments, possibly freeze. It should not read files, open sockets, query a database or print. Three reasons make this a rule rather than a taste:

  • Testability. A constructor that does only in-memory work can be called in a test with plain values.

  • Failure. new has no way to say "the file was missing" except by raising from the constructor, and the half-built object is then unreachable. A named class method that does the I/O can choose its error, retry, or return nil.

  • Clarity. Settings.new reads as "make an object from these values"; Settings.load(path) says that a file is read.

The idiom is a class method that performs the effect and ends by calling new. Keyword parameters with defaults let the file be sparse, and freeze at the end of initialize makes the object immutable (see Object Lifecycle, Freezing and Copying).

# frozen_string_literal: true

require "tmpdir"

# The constructor only stores values. Anything that touches the outside world
# is a named class method that ends by calling new.
class Settings
  attr_reader :loan_days, :currency

  def self.parse(text)
    pairs = text.each_line.filter_map { |line| line.chomp.split("=", 2) if line.include?("=") }
    new(**pairs.to_h { |key, value| [key.to_sym, value] })
  end

  def self.load(path) = parse(File.read(path))

  def initialize(loan_days: "14", currency: "EUR")
    @loan_days = Integer(loan_days)
    @currency = currency
    freeze
  end
end

Dir.mktmpdir do |dir|
  path = File.join(dir, "shelf.conf")
  File.write(path, "loan_days=21\ncurrency=USD\n")
  settings = Settings.load(path)
  p settings
  p settings.frozen?
  p Settings.new
end

# A private new plus named factories closes every other way in.
class LoanPolicy
  attr_reader :days, :renewals

  def self.standard = new(14, 2)
  def self.strict = new(7, 0)

  private_class_method :new

  def initialize(days, renewals)
    @days = days
    @renewals = renewals
  end
end

p LoanPolicy.strict
begin
  LoanPolicy.new(1, 1)
rescue NoMethodError => e
  puts e.message
end
#<Settings:0x00000001055eff50 @loan_days=21, @currency="USD">
true
#<Settings:0x00000001055e96c8 @loan_days=14, @currency="EUR">
#<LoanPolicy:0x0000000124bcecb8 @days=7, @renewals=0>
private method 'new' called for class LoanPolicy

Settings.parse is pure and Settings.load is the only method that touches the file system, so a test can exercise parse with a string. LoanPolicy goes one step further: private_class_method :new removes the public constructor, so the only ways to obtain a policy are the named factories standard and strict. A private new can still be called from inside the class, with an implicit receiver, which is how the factories use it. The same technique is the basis of the singleton pattern (see Singleton Classes and Class-Level Behavior).

Two cautions apply to constructors. First, new is a normal method, so it can be overridden (def self.new(...)) to return an instance of a different class or a cached object, but that surprises readers: prefer a named factory. Second, a subclass that defines initialize without calling super skips the parent’s setup; see Inheritance and super.

Instance variables

An instance variable is a name starting with a single @. It belongs to one object, is created by its first assignment, and is visible only inside that object’s methods. There is no declaration, and two instances of the same class may carry different sets of variables.

# frozen_string_literal: true

class Member
  def initialize(name)
    @name = name
  end

  def nickname? = !@nickname.nil?
end

ada = Member.new("Ada")
p ada.instance_variable_get(:@nickname)
p ada.nickname?
p ada.instance_variables
p ada.instance_variable_get(:@name)
ada.instance_variable_set(:@nickname, "Countess")
p ada.instance_variables
p ada.instance_variable_defined?(:@email)
p ada.remove_instance_variable(:@nickname)
p ada.instance_variables

begin
  ada.instance_variable_get("name")
rescue NameError => e
  puts e.message
end

ada.freeze
begin
  ada.instance_variable_set(:@name, "Bob")
rescue FrozenError => e
  puts e.message
  p e.receiver.equal?(ada)
end
nil
false
[:@name]
"Ada"
[:@name, :@nickname]
false
"Countess"
[:@name]
'name' is not allowed as an instance variable name
can't modify frozen Member: #<Member:0x000000012462bb58 @name="Ada">
true

The facts to remember:

  • Unset reads are nil. Reading @nickname before assigning it returns nil. Before Ruby 3.0 this printed the warning instance variable @nickname not initialized under -w; the warning was removed in 3.0, so a typo in an instance variable name now fails silently (a nil appears later). RuboCop and the tests have to catch it.

  • The reflection methods are public. instance_variables, instance_variable_get, instance_variable_set, instance_variable_defined? and remove_instance_variable (all from Object) work from outside, so instance variables are not private data in the strict sense: they are only unreachable without these methods. The name must be a symbol or string that starts with @; "name" raises NameError: 'name' is not allowed as an instance variable name.

  • Freezing blocks writes. A frozen object raises FrozenError (can't modify frozen Member: ...) on any assignment, including through instance_variable_set; its receiver is the object.

  • The order is the assignment order. instance_variables returns the names in the order they were first assigned. Since Ruby 3.2 the VM stores variables using object shapes keyed by that order, so assigning the same variables in the same order in initialize (the usual style) is also the fastest layout. Define every variable in the constructor, even as nil, so all instances share one shape.

  • Variables are per object, not per class. A class has its own instance variables too (@issued in the example in Class methods and class-level state), unrelated to the variables of its instances.

  • Subclasses share the namespace. An instance variable set by a superclass method and one set by a subclass method with the same name are the same variable. Prefix or choose names carefully when inheriting from a class you do not own.

The related variable kinds are covered elsewhere: @@class_variables, constants and globals in Variables, Constants and Scope.

Attribute methods

Instance variables are not readable from outside, so objects expose state through methods. Three Module methods generate the boring ones:

Call Defines

attr_reader :a, :b

a and b, returning @a and @b.

attr_writer :a

a=, assigning @a.

attr_accessor :a

both a and a=.

attr :a

A legacy alias of attr_reader. The old two-argument form attr :a, true also made a writer; it still works but prints warning: optional boolean argument is obsoleted. RuboCop’s Style/Attr flags every use of attr.

Each of them accepts symbols or strings, and since Ruby 3.0 returns an array of the defined method names as symbols (attr_accessor :three returns [:three, :three=]), which is what makes private attr_reader :x possible: private accepts that array. The generated methods are ordinary public methods unless a visibility is in effect (see Visibility and Encapsulation).

# frozen_string_literal: true

class Copy
  attr_reader :condition
  attr_writer :shelf_mark
  attr_accessor :notes

  def initialize(condition: :good)
    @condition = condition
  end

  def condition=(value)
    raise ArgumentError, "unknown condition: #{value.inspect}" unless %i[new good worn].include?(value)

    @condition = value
  end

  def mark_worn
    self.condition = :worn # the explicit receiver calls the writer
    condition
  end

  def mark_worn_wrongly
    condition = :worn # a new local variable: the object is untouched
    "#{condition} locally"
  end

  def shelf_mark? = !@shelf_mark.nil?

  private attr_reader :shelf_mark
end

result = Class.new do
  attr_reader :one, :two
  attr_accessor :three
end
p result.instance_methods(false)

copy = Copy.new
p(copy.public_methods(false).sort)
p copy.shelf_mark?
copy.shelf_mark = "QA76"
p copy.shelf_mark?
p copy.mark_worn_wrongly
p copy.condition
p copy.mark_worn
p copy.condition
copy.condition = :new
p copy.condition

begin
  copy.condition = :torn
rescue ArgumentError => e
  puts e.message
end

begin
  copy.shelf_mark
rescue NoMethodError => e
  puts e.message
end

copy.notes ||= []
copy.notes << "spine cracked"
p copy.notes
p(copy.then { Copy.instance_method(:notes=).arity })
p Copy.send(:attr_reader, :extra)
[:one, :two, :three, :three=]
[:condition, :condition=, :mark_worn, :mark_worn_wrongly, :notes, :notes=, :shelf_mark=, :shelf_mark?]
false
true
"worn locally"
:good
:worn
:worn
:new
unknown condition: :torn
private method 'shelf_mark' called for an instance of Copy
["spine cracked"]
1
[:extra]

Points the output makes concrete:

  • The setter pitfall. Inside a method, condition = :worn creates a local variable; only self.condition = :worn calls the writer. The second call in the example (mark_worn_wrongly) returns "worn locally" and the object’s condition stays :good. This is the one place where self. is required on a method call (see the implicit-receiver rule in Visibility and Encapsulation), and a classic source of "my setter does nothing" bugs. A related slip is a class with only attr_reader whose code later calls a setter, which raises NoMethodError: the cure is the right attr_* (or a method that changes the state on purpose).

  • Writers can be written by hand. condition= above validates its argument; a hand-written writer, a reader that computes a value, or a reader with a default are all normal. attr_* is only for the trivial case.

  • ||= works through accessors. copy.notes ||= [] expands to copy.notes || (copy.notes = []).

  • Predicates return truthy or falsy values. shelf_mark? returns a boolean here because nil.! produces one, but the convention for ? methods is "answers a question", not "returns exactly true or false`" (`5.nonzero? returns 5).

  • private attr_reader :shelf_mark keeps the reader for internal use while the writer stays public; the call from outside raises private method 'shelf_mark' called for an instance of Copy.

  • Grouping. RuboCop’s Style/AccessorGrouping prefers one attr_reader :a, :b over two calls, and Style/TrivialAccessors prefers attr_reader :nickname to def nickname = @nickname.

to_s and inspect

Two methods turn an object into a string, and they have different audiences:

Method Used by and meant for

to_s

puts, string interpolation ("#{obj}"), print, Kernel#String. A human-readable form, shown to users. The default is #<ClassName> with the object address (#<Member:0x000000012035b3c8>, no instance variables).

inspect

p, irb, the debugger, and the inspect of every container (Array#inspect calls inspect on each element). A programmer-readable form. The default is #<ClassName:0x... @a=1, @b=2> with the instance variables.

Overriding one does not change the other (inspect has not called to_s since Ruby 2.0). to_s must return a String: if it returns anything else, both interpolation and puts silently print the default #<A:0x...> form instead. The shelf gem follows the split: Shelf::Book#to_s returns "Title by Author" while inspect of a Data object shows #<data Shelf::Book isbn=..., title=..., author=...>.

# frozen_string_literal: true

class Member
  def initialize(name, email)
    @name = name
    @email = email
    @loans = []
  end

  def to_s = "member #{@name}"
end

ada = Member.new("Ada", "ada@example.com")
puts ada
puts "Hello, #{ada}"
puts ada.inspect.sub(/0x\h+/, "0x...")
puts [ada].to_s.sub(/0x\h+/, "0x...")

# Ruby 4.0: inspect asks which instance variables to show.
class Account
  def initialize(owner, token)
    @owner = owner
    @token = token
  end

  private

  def instance_variables_to_inspect = [:@owner]
end

puts Account.new("Ada", "s3cr3t").inspect.sub(/0x\h+/, "0x...")

# Overriding inspect changes p, irb and the output of collections.
class Isbn10
  def initialize(digits) = @digits = digits

  def inspect = "#<Isbn10 #{@digits}>"
end

p Isbn10.new("0306406152")
p [Isbn10.new("0306406152")]
x = p(1, 2)
p x
member Ada
Hello, member Ada
#<Member:0x... @name="Ada", @email="ada@example.com", @loans=[]>
[#<Member:0x... @name="Ada", @email="ada@example.com", @loans=[]>]
#<Account:0x... @owner="Ada">
#<Isbn10 0306406152>
[#<Isbn10 0306406152>]
1
2
[1, 2]

What to take from it:

  • puts ada printed the overridden to_s, but the inspect of the same object (and of the array that contains it) is still the default, listing every instance variable. An inspect that dumps @email or a token into a log line is a common leak.

  • Ruby 4.0: instance_variables_to_inspect. Kernel#inspect now checks whether the object has a method named instance_variables_to_inspect and, if it does, shows only the instance variables it returns. The method may be private (the NEWS example makes it so, and the Account above does the same), and there is no need to override inspect itself. The result for Account is #<Account:0x... @owner="Ada">: the token is not printed. It is a convenience for hiding secrets and large values, not a security boundary (instance_variable_get still reads them).

  • Before 4.0 the same effect needs a hand-written inspect, as Isbn10 does. A hand-written inspect should stay short and should not include secrets.

  • p with one argument returns it, with several returns an array, and with none returns nil; it prints inspect for each argument. puts prints to_s and flattens arrays. pp is p with line breaking for nested structures.

  • The 3.4 Hash#inspect format ({name: "Ada"}) and the 3.4 Set/Data formats are what the examples print; older tutorials show {:name=>"Ada"}.

Reopening classes

A class statement for a name that already holds a class does not replace it: it reopens it, and the new body adds methods (or redefines existing ones) on the same object. This is how a class is split across files (a gem’s lib/shelf/book.rb and a later lib/shelf/book/extras.rb), and how the core classes can be extended. The superclass, if written, must match the existing one, and the name must be a class.

# frozen_string_literal: true

require "shelf"

# The first definition creates Shelf::Book (it is a Data class in the gem).
# Another class block with the same name reopens it instead of replacing it.
module Shelf
  class Book
    def isbn_digits = isbn.digits
  end
end

author = Shelf::Author.new("N. K. Jemisin")
book = Shelf::Book.new(isbn: Shelf::Isbn.new("9780306406157"), title: "The Fifth Season", author:)
puts book
puts book.isbn_digits
p Shelf::Book.instance_methods(false).sort

# Reopening a core class is legal too, and is monkey patching.
class Integer
  def days = self * 24 * 60 * 60
end

p 14.days
p Integer.instance_method(:days).owner
p Integer.instance_method(:days).source_location.first.end_with?("e07.rb")
The Fifth Season by N. K. Jemisin
9780306406157
[:author, :isbn, :isbn_digits, :title, :to_s]
1209600
Integer
true

Reopening has sharp edges:

  • Mismatched superclass. Writing class Member < String when Member already exists with another superclass raises TypeError: superclass mismatch for class Member. Omit the < Super when reopening.

  • Not a class. class Mod when Mod is a module raises TypeError: Mod is not a class, and module Member when Member is a class raises Member is not a module (both with a pointer to the previous definition).

  • Redefinition warns under -w. Defining a method that already exists on the same class prints warning: method redefined; discarding old hi followed by warning: previous definition of hi was here, a useful tripwire for accidental overwrites. RuboCop’s Lint/DuplicateMethods catches the same mistake statically.

  • Monkey patching is global. Integer#days above now exists for every library in the process, and two libraries that add a method with the same name silently overwrite each other. The alternatives are a refinement, a wrapper object or a prepended module, covered in Refinements and Monkey Patching. Redefining a basic operator such as Integer#+ makes the VM abandon its fast paths for it, which slows every caller.

  • Reopening also reopens a typo. class Shelf::Bok creates a brand-new class instead of failing. In a gem, define classes inside nested module blocks (as above) and let a test or an autoloader notice stray constants.

The example below is a script that shows the first of those errors, so it ends with an uncaught exception:

# frozen_string_literal: true

class Member
  def name = "Ada"
end

class Member < String
end
e07b.rb:7:in '<main>': superclass mismatch for class Member (TypeError)

Class methods and class-level state

A method defined with def self.name (or inside class << self) is a class method: it belongs to the class object itself and is called on it (LibraryCard.issue("Ada")). Technically it is an instance method of the class’s singleton class, which is why class methods are inherited by subclasses and why private_class_method exists (see Singleton Classes and Class-Level Behavior for the mechanics). Inside a class body self is the class, which makes a class a place for state of its own:

State Where it lives and how it behaves

A constant (MAX_ACTIVE = 3)

Looked up lexically, then through the ancestors. Visible to subclasses (GuestCard::MAX_ACTIVE works). Reassigning prints warning: already initialized constant. A constant holding a mutable object should be frozen.

A class-level instance variable (@issued set in the class body)

An instance variable of the class object. Each class, including each subclass, has its own. Reached with attr_reader inside class << self.

A class variable (@@issued)

One variable shared by the class and all its subclasses. It leaks between hierarchies, and reading it before it is set raises NameError: uninitialized class variable @@x in Member. RuboCop’s Style/ClassVars flags it. Prefer a class-level instance variable.

A global ($issued)

One per process. Avoid.

# frozen_string_literal: true

# self inside a class body is the class itself.
class LibraryCard
  puts "defining #{inspect} (#{self.class})"

  MAX_ACTIVE = 3

  @issued = 0 # an instance variable of the class object

  class << self
    attr_reader :issued

    def issue(holder)
      @issued += 1
      new(holder, @issued)
    end
  end

  def self.reset! = @issued = 0

  attr_reader :holder, :number

  def initialize(holder, number)
    @holder = holder
    @number = number
  end

  def to_s = "card ##{number} for #{holder} (max #{MAX_ACTIVE})"
end

puts LibraryCard.issue("Ada")
puts LibraryCard.issue("Grace")
p LibraryCard.issued
p LibraryCard.singleton_methods.sort
p LibraryCard.instance_variables
p LibraryCard.new("Alan", 99).instance_variables

# Subclasses have their own class-level instance variables.
class GuestCard < LibraryCard
end

begin
  GuestCard.issue("Guest")
rescue NoMethodError => e
  puts e.message
end
p GuestCard.issued
p LibraryCard.constants
p GuestCard::MAX_ACTIVE
defining LibraryCard (Class)
card #1 for Ada (max 3)
card #2 for Grace (max 3)
2
[:issue, :issued, :reset!]
[:@issued]
[:@holder, :@number]
undefined method '+' for nil
nil
[:MAX_ACTIVE]
3

GuestCard.issue inherited the class method and ran it with self set to GuestCard, whose own @issued is nil: the error undefined method '+' for nil is the classic result of class-level state that a subclass never initialised. The fixes are an inherited hook that initialises each subclass (see Hooks, eval and DSLs) or reading the value with || 0. Instance methods can reach class state through self.class, as LibraryCard does for MAX_ACTIVE (a bare constant name is lexically scoped and needs no receiver).

The two usual ways to write a class method, equivalent in effect:

# frozen_string_literal: true

class Library
  # a single method on the class
  def self.open = new

  # many methods at once, and the only form where private and attr_* apply
  class << self
    def close = nil
  end
end

p Library.singleton_methods.sort
[:close, :open]

A third spelling, def Library.reopen, names the class instead of self; it works but RuboCop’s Style/ClassMethods asks for self., so that the method survives renaming the class.

def self.x is the usual choice for one or two methods; class << self is clearer for many, and it is the only form that lets private, attr_reader and alias_method apply to the class level. Both define methods on the singleton class, so LibraryCard.singleton_methods lists them. Class methods are inherited: GuestCard.singleton_methods includes issue, while GuestCard.singleton_methods(false) is empty.

A good habit: put behaviour that needs no instance state (parsing a string, building an object from a different representation) in a class method, and keep instance methods for behaviour that depends on the object’s own variables. A class method that takes an instance as an argument is usually an instance method in disguise.

Inheritance and super

class Dvd < Media makes Media the superclass of Dvd. Instances of Dvd get every instance method of Media and may override them; Dvd also inherits the class methods and constants of Media. Ruby has single inheritance: a class has exactly one superclass, and the rest of the reuse is done with modules (see Modules and Mixins). When no superclass is written it is Object.

Inside a method, super calls the same-named method in the next class up the chain, and it has three forms:

Form What it passes

super

The current values of the method’s parameters, positional and keyword, and the block (an implicit pass-through).

super()

Nothing: no arguments, but the block is still passed (use super(&nil) to also drop the block).

super(a, b)

Exactly those arguments (and the block).

The lookup path that super follows, the way modules take part in it, and Method#super_method are the subject of Method Lookup and super.

# frozen_string_literal: true

class Media
  attr_reader :title

  def initialize(title)
    @title = title
  end

  def to_s = "#{self.class.name.downcase}: #{title}"
  def loan_days = 14
end

class Dvd < Media
  attr_reader :minutes

  def initialize(title, minutes:)
    super(title) # explicit arguments
    @minutes = minutes
  end

  # bare super passes the current arguments (none here)
  def loan_days = super - 7
end

class AudioBook < Media
  def initialize(title, narrator)
    super # bare super passes title and narrator: too many for Media#initialize
    @narrator = narrator
  end
end

dvd = Dvd.new("Metropolis", minutes: 153)
puts dvd
p dvd.loan_days
p Dvd.superclass
p Dvd.ancestors.take(3)
p Media.subclasses.sort_by(&:name)
p Dvd.instance_method(:to_s).owner
p dvd.method(:loan_days).super_method.owner

begin
  AudioBook.new("Dune", "Scott Brick")
rescue ArgumentError => e
  puts e.message
end
dvd: Metropolis
7
Media
[Dvd, Media, Object]
[AudioBook, Dvd]
Media
Media
wrong number of arguments (given 2, expected 1)

Reading the output:

  • Dvd#to_s is not defined; Dvd.instance_method(:to_s).owner is Media, the class that supplies it. to_s uses self.class.name, so a subclass gets a correct label without overriding the method: the template method idea in one line.

  • Dvd.ancestors.take(3) is [Dvd, Media, Object]: the chain super walks.

  • Class#subclasses (Ruby 3.1) returns the direct subclasses currently loaded, in no guaranteed order, so the example sorts them. It is the supported replacement for scanning ObjectSpace.each_object(Class), and subclasses that were garbage collected are absent. There is no descendants in 4.0.

  • The AudioBook error is the best-known super trap. A bare super in initialize(title, narrator) forwards both arguments, while Media#initialize takes one. ArgumentError is raised, with the usual message, from inside Media. When the signatures differ, write super(title). This is also how to correct tutorials that claim bare super passes the original arguments: it passes the current values of the parameters (including reassigned ones, and keywords).

  • If a subclass defines initialize and never calls super, the parent’s instance variables are never set. RuboCop’s Lint/MissingSuper flags a missing super in initialize (and in the callbacks inherited, included, …​), except in classes with no explicit superclass.

Inheritance means "is a": Dvd is a Media everywhere a Media is expected, and callers may rely on that (the Liskov substitution idea in SOLID and Design Patterns). Ruby does not enforce it, so overriding with a different return type or stricter arguments breaks callers silently. Ruby also does not let you inherit from everything: Class.new(Class) and class Foo < Integer; end; Foo.new fail, and inheriting from core classes with their own allocation (Array, Hash, String) needs care, see Designing classes.

Abstract methods

Ruby has no abstract keyword. The usual way to define a method that subclasses must provide is to raise from the base class. The exception to pick matters, because the first reflex, NotImplementedError, is the wrong one for most code:

  • NotImplementedError is a subclass of ScriptError, not of StandardError. It was designed to signal that a feature is unavailable on the platform (fork on Windows). A bare rescue (which rescues StandardError) does not catch it, nor does rescue => e, so a missing method in a subclass bypasses the application’s error handling and ends the program. The Ruby documentation describes it as the error for a feature that the current platform does not implement.

  • NoMethodError says exactly what has happened (a required method is absent), is a StandardError, and is what Ruby raises when a method is not defined at all.

  • A custom error under the library’s base error (Shelf::Error, see Exceptions and Error Handling) lets callers rescue the whole library.

# frozen_string_literal: true

class Exporter
  # The abstract operation: subclasses must supply it.
  def render(_books) = raise(NoMethodError, "#{self.class} must implement #{__method__}")

  def export(books) = render(books).strip
end

class CsvExporter < Exporter
  def render(books) = books.join(",")
end

class BrokenExporter < Exporter
end

p CsvExporter.new.export(%w[Dune Emma])

begin
  BrokenExporter.new.export([])
rescue NoMethodError => e
  puts e.message
end

# NotImplementedError is a ScriptError, not a StandardError.
p NotImplementedError.ancestors.take(4)

begin
  begin
    raise NotImplementedError, "not yet"
  rescue StandardError
    puts "bare rescue caught it"
  end
rescue ScriptError => e
  puts "only ScriptError caught #{e.class}"
end
"Dune,Emma"
BrokenExporter must implement render
[NotImplementedError, ScriptError, Exception, Object]
only ScriptError caught NotImplementedError

Exporter#export is the stable skeleton and render the hook, a small instance of the template method pattern. Many books and tutorials show raise NotImplementedError in the base class. That is the pattern to avoid, for the reason the last lines of the output demonstrate. A related trap is raise NotImplementedError, "..." inside code that is wrapped in rescue => e: the rescue does not trigger and the process exits with the error. An alternative that needs no base-class method at all is duck typing: call the method and let the NoMethodError of the missing one speak.

Anonymous classes and Class.new

Class.new(superclass = Object) { body } creates a class at run time. The block runs with self set to the new class (it is evaluated like a class body), so def, attr_reader and include work in it, but constants and @@vars resolve lexically at the block, not in the class. The class has no name until it is assigned to a constant:

# frozen_string_literal: true

anonymous = Class.new do
  def hello = "hello from #{self.class.inspect}"
end

p anonymous.name
puts anonymous.new.hello.sub(/0x\h+/, "0x...")
puts anonymous.inspect.sub(/0x\h+/, "0x...")

Object.const_set(:Greeter, anonymous) # the first constant assignment names the class
p anonymous.name
puts anonymous.new.hello

Named = Class.new { def hello = "I know my name" }
p Named.name

p Class.new(StandardError).superclass

# Ruby 3.3: give an anonymous class a temporary name for error messages.
temporary = Class.new.set_temporary_name("fake<Report>")
p temporary

# Class.new with a parent and a body, as a parameterised subclass.
def error_class(prefix)
  Class.new(StandardError) do
    define_method(:message) { "#{prefix}: #{super()}" }
  end
end

ShelfError = error_class("shelf")
begin
  raise ShelfError, "no copies left"
rescue ShelfError => e
  puts e.message
  p e.class.name
end

begin
  Class.new(Comparable)
rescue TypeError => e
  puts e.message
end

begin
  Class.new(Class)
rescue TypeError => e
  puts e.message
end

begin
  Class.new(anonymous.singleton_class)
rescue TypeError => e
  puts e.message
end
nil
hello from #<Class:0x...>
#<Class:0x...>
"Greeter"
hello from Greeter
"Named"
StandardError
fake<Report>
shelf: no copies left
"ShelfError"
superclass must be an instance of Class (given an instance of Module)
can't make subclass of Class
can't make subclass of singleton class

Things to notice:

  • The name appears at the first assignment to a constant (Named = Class.new { ... }, const_set, or a class statement later). Class.new alone yields nil as name and #<Class:0x...> as inspect.

  • Module#set_temporary_name (Ruby 3.3) gives an anonymous class a readable label for messages without making it a real constant; the label may not look like a constant name, which is why it is written fake<Report>.

  • Class.new(parent) takes a class: a module raises superclass must be an instance of Class (given an instance of Module), Class itself cannot be subclassed (can't make subclass of Class), and neither can a singleton class (can't make subclass of singleton class).

  • define_method and the closure over prefix are what a class statement cannot do: the class is parameterised by a local variable. Remember that super() inside define_method needs explicit parentheses, because implicit super is not allowed there.

  • Anonymous classes appear in error messages as an instance of #<Class:0x...>, which is hard to read in logs. Use set_temporary_name or name them.

  • Struct.new and Data.define return anonymous classes in the same way, which is why Book = Data.define(...) gives the class its name. See Struct, Data and Value Objects.

Asking about classes and objects

Ruby code usually asks what an object can do rather than what it is (see Design Philosophy and Coming from Other Languages), but there are a few tools to ask what it is, from the strict to the loose:

Test True when

obj.instance_of?(K)

obj.class is exactly K (subclasses and modules do not count).

obj.is_a?(K) / obj.kind_of?(K)

K is the class of obj, a superclass of it, or a module included (or prepended) in it. kind_of? is an alias.

K === obj

The same as obj.is_a?(K) for a class or module K. This is the operator that case/when and rescue use.

K1 < K2, K1 <= K2

K1 is a strict descendant (or the same class). nil when the two are unrelated.

K1 <=> K2

-1, 0, 1 for descendant, same, ancestor; nil when unrelated or when the other side is not a module.

K.include?(M)

The module M is among the ancestors of K (for classes, only modules, not superclasses).

obj.respond_to?(:m)

The object has a public method m (pass true as the second argument to include private ones).

K.instance_methods(false)

The public and protected methods defined in K itself.

# frozen_string_literal: true

require "shelf"

class Media
  def kind = "media"
end

class Dvd < Media
end

module Lendable
end

class AudioBook < Media
  include Lendable
end

dvd = Dvd.new
audio = AudioBook.new

p [dvd.is_a?(Dvd), dvd.is_a?(Media), dvd.is_a?(Object), dvd.is_a?(Lendable)]
p dvd.method(:kind_of?) == dvd.method(:is_a?)
p [dvd.instance_of?(Dvd), dvd.instance_of?(Media)]
p [audio.is_a?(Lendable), audio.instance_of?(AudioBook)]
p [Media === dvd, Dvd === audio, Lendable === audio] # rubocop:disable Style/CaseEquality
same = Dvd
p [Dvd < Media, Media < Dvd, same <= Dvd, same < Dvd, Dvd < AudioBook]
p [Dvd <=> Media, Media <=> Dvd, Dvd <=> AudioBook, Dvd <=> 3]
p Dvd.ancestors.include?(Media)
p Dvd.include?(Lendable)
p AudioBook.include?(Lendable)
p [dvd.respond_to?(:to_s), dvd.respond_to?(:initialize), dvd.respond_to?(:initialize, true)]
p dvd.class.instance_methods(false)
p AudioBook.included_modules.first
p 3.is_a?(Comparable)

label =
  case audio
  when Dvd then "disc"
  when Lendable then "lendable"
  else "other"
  end
puts label

p Shelf::Isbn.ancestors.take(2)
p Shelf::Isbn.superclass
p Shelf::Book.superclass
[true, true, true, false]
true
[true, false]
[true, true]
[true, false, true]
[true, false, true, false, nil]
[-1, 1, nil, nil]
true
false
true
[true, false, true]
[]
Lendable
true
lendable
[Shelf::Isbn, Data]
Data
Data

Remarks:

  • is_a? follows modules: audio.is_a?(Lendable) is true because AudioBook includes it, while dvd.is_a?(Lendable) is false. dvd.instance_of?(Media) is false even though a Dvd is a Media; instance_of? is rarely what you want (RuboCop’s Style/ClassEqualityComparison steers away from obj.class == K for the same reason).

  • Lendable === audio in a case is what allows when Lendable to select by role. The cop Style/CaseEquality objects to calling === directly; the line is annotated with rubocop:disable because the example is about the operator.

  • Dvd <=> 3 is nil: comparing a class with a non-module is not an error.

  • respond_to?(:initialize) is false because initialize is private; the second argument true includes private methods.

  • Shelf::Isbn.ancestors.take(2) is [Shelf::Isbn, Data]: classes made with Data.define inherit from Data, and Comparable does not appear because only Integer, String, Time and a few others include it.

  • A type check at a boundary is fine (raise ArgumentError unless value.is_a?(Shelf::Isbn)); a chain of is_a? tests in the middle of logic usually means a method should be polymorphic. A case over classes is the idiom when the types are genuinely closed. See Pattern Matching for case/in on objects.

BasicObject, Object and Kernel

Three names sit at the root of every class hierarchy. They are easy to mix up, and each has a distinct job:

Name Role

BasicObject

The root class and the only class with no superclass (BasicObject.superclass is nil). It has the bare minimum: ==, !=, !, equal?, instance_eval, instance_exec, __id__, __send__, plus the private initialize, method_missing, and the singleton_method_added, singleton_method_removed and singleton_method_undefined hooks. No inspect, no class, no is_a?, no puts.

Object

The default superclass: a class declared without < inherits from Object. It mixes in Kernel, so everything below is available to almost every object. It defines inspect, to_s, instance_variable_get, is_a?, respond_to?, dup, freeze, send/public_send, methods, singleton_class, then, tap, itself and many more.

Kernel

A module, included by Object. It holds the functions that look like built-in statements (puts, print, p, require, raise, format, Integer(), loop, lambda, rand, sleep, catch/throw). They are private instance methods (so only callable with an implicit receiver, like puts "x" from any object) and also public singleton methods (Kernel.puts).

classDiagram direction BT class BasicObject class Object class Module class Class class Kernel { <<module>> puts() raise() Integer() } class Member Object --|> BasicObject Module --|> Object Class --|> Module Member --|> Object Object ..> Kernel : includes

Class is a subclass of Module, which is a subclass of Object: a class is an object, and a module is a class’s parent in the sense that a class is a module that can be instantiated. This is why Member.class is Class, Class.class is Class too (the chain loops back on itself), and why Class.ancestors is [Class, Module, Object, Kernel, BasicObject].

# frozen_string_literal: true

p Class.ancestors
p Class.superclass
p Class.class
p [Object.superclass, BasicObject.superclass]
p BasicObject.instance_methods.sort
p BasicObject.private_instance_methods.sort
p Kernel.instance_method(:puts).owner
p Kernel.private_method_defined?(:puts)
p Object.new.respond_to?(:puts)
p Object.new.respond_to?(:then)

# A BasicObject subclass is a blank slate: no inspect, no is_a?, no puts.
class Proxy < BasicObject
  def initialize(target) = @target = target

  def method_missing(name, ...)
    ::Kernel.puts "-> #{name}"
    @target.public_send(name, ...)
  end

  def respond_to_missing?(name, include_private = false) = @target.respond_to?(name, include_private)
end

proxy = Proxy.new("shelf")
p proxy.upcase
p proxy.length
p proxy.class

begin
  BasicObject.new.inspect
rescue NoMethodError => e
  puts e.message
end
p Proxy.instance_method(:__id__).owner

puts Object.new.inspect.sub(/0x\h+/, "0x...")
puts Object.new.to_s.sub(/0x\h+/, "0x...")
p Object.new.frozen?
p(5.then { it + 1 })
p Kernel.respond_to?(:puts)
p Kernel.singleton_class.include?(Kernel)
[Class, Module, Object, Kernel, BasicObject]
Module
Class
[BasicObject, nil]
[:!, :!=, :==, :__id__, :__send__, :equal?, :instance_eval, :instance_exec]
[:initialize, :method_missing, :singleton_method_added, :singleton_method_removed, :singleton_method_undefined]
Kernel
true
false
true
-> upcase
"SHELF"
-> length
5
-> class
String
undefined method 'inspect' for an instance of BasicObject
BasicObject
#<Object:0x...>
#<Object:0x...>
false
6
true
true

What the output shows:

  • BasicObject is nearly empty. Its eight public instance methods are !, !=, ==, __id__, __send__, equal?, instance_eval and instance_exec. A subclass of it (Proxy) is therefore a blank slate: calling inspect on a plain BasicObject raises undefined method 'inspect' for an instance of BasicObject. Inside such a class, constants resolve lexically, so core classes need a :: prefix (::Kernel.puts).

  • method_missing is where the blank slate pays off. Proxy forwards every other call to the wrapped object; even proxy.class is forwarded, so it reports the target’s class String. A proxy that must answer respond_to? correctly also needs respond_to_missing?, see Dynamic Dispatch and Method Definition. Delegator and SimpleDelegator (Modules and Mixins) are the usual way; subclassing BasicObject is for the rare cases that must forward even inspect or ==.

  • Kernel functions are private. Object.new.respond_to?(:puts) is false, yet puts works from inside any object because the call has an implicit receiver. Because they are module_function-style, Kernel.puts "x" also works with an explicit receiver. Calling obj.puts from outside raises private method 'puts' called for an instance of ....

  • Object methods are public. then, tap, itself, frozen?, instance_of? and the reflection methods can be called from anywhere. Object#then (aliased yield_self) passes the receiver to a block and returns the block’s value, handy with it.

  • Classes are Object`s. Everything an `Object can do, a class can: Member.respond_to?(:then), Member.frozen?, Member.instance_variables, Member.inspect. That is why class-level state (see Class methods and class-level state) needs no special mechanism.

Outside a BasicObject subclass you almost never need to think about this layering. It matters when you override method_missing, build a proxy, define a DSL base class (a BasicObject subclass prevents puts or exit from leaking into the DSL’s scope; see Hooks, eval and DSLs), or read Object.instance_methods to see why a name is already taken. When you define your own to_s you override Kernel#to_s/Object#to_s; super still reaches it.

The shelf classes

The running shelf gem is a small library catalogue whose classes cover the page’s ideas: value objects made with Data.define (Isbn, Author, Book), a state-driven Loan, a duck-typed Store::Memory, an Enumerable Catalog, a configuration Struct, and an error class that inherits from StandardError.

classDiagram class Isbn { <<Data>> +String digits +to_s() String } class Author { <<Data>> +String name } class Book { <<Data>> +Isbn isbn +String title +Author author +to_s() String } class Loan { +Book book +Symbol state +describe() String } class Memory { +all() Array +add(book) Memory } class Catalog { <<Enumerable>> +add(book) Catalog +each() Enumerator } class Config { <<Struct>> +store +loan_days } class Error class StandardError Book *-- Isbn Book *-- Author Loan o-- Book Catalog o-- Memory : store Config o-- Memory : store Error --|> StandardError

The solid diamonds mean that a Book is made of an Isbn and an Author (composition: they have no life apart from it), the open diamonds that Loan and Catalog merely hold references to collaborators passed in. Catalog talks to its store through two methods (all and add), not through a class: any object that answers both can be substituted, which is the duck-typing and composition pattern developed in the next section. Inheritance appears only where it means "is a" (Error is a StandardError), and the three Data classes inherit from Data.

Designing classes

A class is a promise about a small set of responsibilities. The following rules are the practical output of the object-oriented material above.

Keep constructors cheap and honest

As in Constructors without I/O: initialize assigns, validates and freezes; it does not read files, open connections, print or start threads. When an object needs one of those, take it as an argument and let a factory (Settings.load) do the effect. When validation is non-trivial, as in Shelf::Isbn, raise ArgumentError from initialize so no invalid object is ever visible.

Prefer composition over inheritance

Inheritance fixes the relationship at definition time, exposes the parent’s whole interface and couples the child to the parent’s internals. Composition lets the collaborator be chosen when the object is built. The rule of thumb: inherit when the child is a kind of parent for all of the parent’s clients (Dvd is a Media); otherwise hold the collaborator in an instance variable and forward the few methods needed. Deep chains are a design smell: each level adds methods the reader must know, and a change in a base class can break every descendant (the "fragile base class" problem). Two or three levels is normal; more usually means reuse via modules or composition is wanted.

# frozen_string_literal: true

require "shelf"
require "stringio"

# One job: turn a loan into text. No I/O, easy to test.
class Receipt
  def initialize(book, member, days)
    @book = book
    @member = member
    @days = days
  end

  def to_s = "#{@member} borrowed #{@book.title} for #{@days} days"
end

# One job: deliver text. Any object with #deliver(text) can replace it.
class StreamNotifier
  def initialize(io) = @io = io

  def deliver(text) = @io.puts(text)
end

# The desk coordinates collaborators that are passed in, not created inside.
class Desk
  def initialize(store: Shelf::Store::Memory.new, notifier: StreamNotifier.new($stdout))
    @store = store
    @notifier = notifier
  end

  def check_out(book, member)
    @store.add(book)
    receipt = Receipt.new(book, member, Shelf.config.loan_days)
    @notifier.deliver(receipt.to_s)
    receipt
  end

  def count = @store.all.size
end

author = Shelf::Author.new("Italo Calvino")
book = Shelf::Book.new(isbn: Shelf::Isbn.new("9780306406157"), title: "Invisible Cities", author:)

log = StringIO.new
desk = Desk.new(notifier: StreamNotifier.new(log))
desk.check_out(book, "Ada")
p log.string
p desk.count
p Desk.public_instance_methods(false).sort

# Subclassing a core class couples you to its whole interface.
class Stack < Array
end

stack = Stack.new([1, 2, 3])
p stack.class
p stack.map { it * 2 }.class
p stack.select(&:odd?).class
p stack.dup.class
p Stack.instance_methods.size > 100
"Ada borrowed Invisible Cities for 14 days\n"
1
[:check_out, :count]
Stack
Array
Array
Stack
true

Desk has two public methods (check_out and count), and it never creates its collaborators inside check_out: the store and the notifier arrive through the constructor with defaults (constructor injection, the pattern the SOLID page names dependency inversion; see SOLID and Design Patterns). A test passes a StringIO and reads log.string; production passes $stdout. Neither Receipt nor StreamNotifier knows about the others. Adding an EmailNotifier needs no change to Desk.

The Stack < Array lines show the cost of inheriting from a core collection. The subclass inherits the entire Array interface (Stack.instance_methods.size is well over a hundred), so stack << :anything, sort! and clear all remain public even when a stack should offer only push and pop. map and select on an Array subclass return a plain Array (and since Ruby 3.0 so do take, drop, slice, uniq, flatten and *), while dup returns a Stack: the rules differ by method and by version. Wrapping the array in an instance variable and exposing push, pop and each is cleaner. The same warning applies to Hash and String.

Small public surface, one reason to change

A God class is one that knows and does too much: it formats, stores, validates, sends e-mail and reports. It collects every change request and every bug. The symptoms are a long list of unrelated instance variables, methods that use disjoint subsets of them, a name ending in Manager, Helper or Handler, and a test file that needs elaborate set-up. The cure is to extract one collaborator per responsibility (Receipt, StreamNotifier) and let the original class coordinate. A related rule is to limit the public methods: everything that is not part of the contract is private, because every public method is something clients may call and you must keep working. See Visibility and Encapsulation.

Further conventions that make a class easy to read:

  • Order. Put constants and include`s first, then the class methods, `initialize, the public methods, and private at the end, with each private helper after the first method that uses it (the "stepdown" order, so that the file reads top to bottom from the general to the detail). RuboCop’s Layout/ClassStructure can enforce an order; it is disabled by default.

  • Name by role. A class name is a noun (Receipt, Catalog); a method name is a verb or a property (check_out, count); a predicate ends in ? (nickname?) and the dangerous twin of a method ends in !. Constants are SCREAMING_SNAKE_CASE, files are snake_case.rb matching the class (library_card.rb for LibraryCard).

  • Extract mixins for reusable behaviour. When two unrelated classes need the same method, put it in a module rather than in a shared superclass (see Modules and Mixins).

  • Value objects are immutable. A small class that is defined by its values should freeze itself in the constructor and implement equality on the values; Data.define does both (see Struct, Data and Value Objects and Equality, Hashing and Comparison).

  • Document with comments that say why. A class comment states the responsibility in a sentence (RuboCop’s Style/Documentation requires one; the shelf gem turns it off). See Documentation with RDoc and YARD.

Where older books and tutorials are outdated

Ruby books and articles written for Ruby 2.x and early 3.x still describe classes correctly in outline, but several details moved:

Older statement Current behaviour

NotImplementedError is the way to mark an abstract method

It is a ScriptError, so a bare rescue does not catch it. Raise NoMethodError or a custom error; see Abstract methods.

Error messages quote the method name with a backtick and end in for nil:NilClass

Ruby 3.3 changed the receiver text and 3.4 the quote: undefined method 'x' for nil, for an instance of Member, for class Integer.

Hash#inspect and p of an object with a hash show {:name=>"Ada"}

Ruby 3.4 prints {name: "Ada"}; Data and Set objects have their own 3.4 and 4.0 formats.

class Book < Struct.new(:isbn) or class Book < Data.define(:isbn)

Assign the generated class to a constant, Book = Data.define(:isbn) do ... end. Subclassing creates a needless extra anonymous class.

A class with only attr_reader and code that later calls the setter

That raises NoMethodError. Declare attr_writer or attr_accessor, or (better) change the state in a method with a meaningful name.

Bare super passes the arguments the method was originally called with

It passes the current values of the parameters, with keywords and the block. When the parent takes fewer arguments, write super(args) or super().

`@instance

= new` in a class method makes a singleton

It is not thread-safe; use the Singleton module (see Singleton Classes and Class-Level Behavior).

Reading an uninitialised instance variable prints a warning under -w

The warning was removed in Ruby 3.0. A typo now yields nil without a message.

A list of the object’s instance variables in inspect can only be changed by overriding inspect

Ruby 4.0 asks the object for instance_variables_to_inspect first; see to_s and inspect.

subclasses needs ObjectSpace.each_object(Class) or a gem

Class#subclasses exists since 3.1; Class#attached_object (3.2) goes from a singleton class back to its object.

Naming an anonymous class requires a constant

Module#set_temporary_name (3.3) gives a temporary label.

_1 is the implicit block parameter

it (Ruby 3.4) is the idiom for one-argument blocks, and RuboCop’s Style/ItBlockParameter prefers it.

Two further points where careless advice circulates. safe_constantize, constantize and classify are ActiveSupport methods (Rails), not core Ruby: in plain Ruby the way to look a class up from a string is Object.const_get("Shelf::Book"), which raises NameError for an unknown name, and which should not be given untrusted input. And Class objects are not special syntax: Class.new, Module.new and Struct.new are ordinary method calls.

Common exceptions and warnings

All of these were reproduced on Ruby 4.0.7 (a scratch Member class with no members unless noted).

Error or warning Message (4.0) Cause and fix

ArgumentError

wrong number of arguments (given 1, expected 0)

new with arguments on a class with no initialize (Object.new(1)), or any arity mismatch. Add the parameter or fix the call.

NoMethodError

undefined method 'nope' for an instance of Member

The receiver’s class has no such method. A typo, a missing attr_*, or a method defined on a different class.

NoMethodError

undefined method 'new' for class Integer

Class without new: Integer, Symbol, NilClass, TrueClass, FalseClass. Use literals.

NoMethodError

private method 'new' called for class LoanPolicy

private_class_method :new; use the factory.

NoMethodError

private method 'shelf_mark' called for an instance of Copy

Calling a private reader with an explicit receiver (outside the object).

NoMethodError

super: no superclass method 'go' for an instance of Member

super in a method that does not override anything.

NoMethodError

undefined method '+' for nil

An instance variable (often a class-level one in a subclass) was never initialised.

NameError

undefined local variable or method 'nothing' for an instance of Member

A bare identifier that is neither a local variable nor a method.

NameError

uninitialized constant Memberr / uninitialized constant Member::Nope

Typo, missing require, or wrong namespace.

NameError

uninitialized class variable @@x in Member

A class variable was read before being set.

NameError

'name' is not allowed as an instance variable name

instance_variable_get("name"): the name must start with @.

NameError

undefined method 'zzz' for class 'Member'

instance_method(:zzz) or method(:zzz) for an unknown method.

TypeError

superclass mismatch for class Member

Reopening a class with a different < Super.

TypeError

Mod is not a class / Member is not a module

The name already holds a module (or class). Rename.

TypeError

superclass must be an instance of Class (given an instance of Module)

class Foo < Mod or Class.new(Mod).

TypeError

can't make subclass of Class / can't make subclass of singleton class

Not allowed.

TypeError

allocator undefined for Integer

Integer.allocate. Core classes without an allocator cannot be instantiated.

TypeError

can't instantiate uninitialized class

Class.allocate.new.

FrozenError

can't modify frozen Member: #<Member:0x...>

Writing an instance variable of a frozen object (including instance_variable_set).

NotImplementedError

message of your choice

Not a StandardError; a bare rescue misses it. Use NoMethodError.

Warning (-w)

method redefined; discarding old hi / previous definition of hi was here

The same method defined twice in a class.

Warning

already initialized constant X / previous definition of X was here

A constant was reassigned.

Warning (-w)

optional boolean argument is obsoleted

attr :a, true.

RuboCop cops for classes

Cop What it checks

Style/Documentation

Every class and module has a comment above it (off in the shelf configuration).

Style/ClassAndModuleChildren

Nested style (module Shelf; class Book) is the default; the compact style (class Shelf::Book) is configurable.

Style/OneClassPerFile

A file defines at most one top-level class or module (added in RuboCop 1.85; the example configuration turns it off because several short classes share a file).

Style/ClassVars

Forbids @@variables.

Style/ClassMethods

Prefers def self.x to def ClassName.x.

Style/ClassCheck

Prefers is_a? to kind_of?.

Style/ClassEqualityComparison

Prefers is_a? or instance_of? to obj.class == K.

Style/CaseEquality

Flags direct === calls.

Style/RedundantSelf

Flags self. where it is not needed (readers and ordinary methods); keep it for setters.

Style/TrivialAccessors

Prefers attr_reader/attr_writer to hand-written trivial accessors.

Style/AccessorGrouping

Groups accessors of the same kind in one call.

Style/Attr

Flags attr.

Lint/EmptyClass

A class or metaclass without a body.

Style/EmptyClassDefinition

One consistent style for empty classes: the class keyword (default) or Class.new.

Lint/MissingSuper

A subclass initialize or a hook such as inherited must call super.

Lint/DuplicateMethods

The same method defined twice in one class.

Lint/UselessAccessModifier

private with nothing after it or private repeated.

Lint/ConstantDefinitionInBlock

A constant defined in a block (such as Class.new { X = 1 }) is created at the top level, not in the class.

Naming/PredicateMethod

Methods that return a boolean end with ?, and ? methods return a boolean (a newer cop; see the earlier remark that the language itself only expects a truthy or falsy answer).

Naming/PredicatePrefix

Predicates should not start with is_, has_ or have_.

Naming/AccessorMethodName

Methods named get_x/set_x should be x/x=.

Metrics/ClassLength, Metrics/AbcSize

Size limits that detect God classes and fat methods.

Layout/ClassStructure

Optional: enforces the order of constants, includes, class methods, initialize, public, private.

Those cops are documented in Style, Lint, Naming, Layout and Metrics. The rest of the toolbox is on Style, Linting and Static Analysis.

Pitfalls

  • Forgetting self. on a setter. name = "x" inside a method sets a local variable; self.name = "x" calls the writer.

  • A subclass that skips super in initialize. The parent’s variables are nil, and the failure appears much later.

  • Bare super with a different signature. It forwards all current arguments; use super(a) or super().

  • Typos in instance variable names. No warning since 3.0; the variable reads as nil. Cover the class with tests.

  • Class variables. @@x is shared by the whole hierarchy; use a class-level instance variable (and remember it is per class: initialise it in inherited or read it with ||=).

  • Subclassing core collections. The subclass inherits the whole interface and surprising return types; wrap instead.

  • NotImplementedError for abstract methods. It escapes rescue => e. Use NoMethodError or a library error.

  • Monkey patching core classes. The method exists for every library in the process; prefer a refinement or a wrapper.

  • Heavy constructors. I/O, threads or network in initialize make objects hard to test and failures hard to recover.

  • Mutable defaults. def initialize(items = []) is fine (a new array each call), but a constant DEFAULT = [] shared across instances is mutated by all of them; freeze it or build a new one.

  • Public readers of mutable state. attr_reader :loans hands out the internal array; return @loans.dup or a frozen copy when callers must not change it.

  • to_s returning a non-String. Interpolation and puts silently fall back to the default #<Class:0x...> form.

  • God classes and deep chains. Extract collaborators; keep inheritance shallow.

Version notes

Version Change

2.0

inspect no longer calls to_s. Module#prepend and refinements (experimental, stable in 2.1).

3.0

attr_reader, attr_writer, attr_accessor and attr return an array of the defined method names as symbols. The warning for reading an uninitialised instance variable is removed. take, drop, uniq, flatten and similar Array methods return Array for subclasses.

3.1

Class#subclasses.

3.2

Class#attached_object. Data.define (immutable value classes). Object shapes make instance variable access faster and reward a consistent assignment order.

3.3

Module#set_temporary_name. NoMethodError messages say for an instance of Member instead of calling inspect on the receiver.

3.4

Error messages use 'method' instead of a backtick; Hash#inspect prints {name: "Ada"}. it as an implicit block parameter.

4.0

Kernel#inspect honours instance_variables_to_inspect. Set is a core class. Kernel#open with a leading | (a pipe command) is removed.