How Ruby Works: Interpreted and Object-Oriented

Published by Programming Line in Ruby. Last updated .

Ruby is both an interpreted language and a thoroughly object-oriented one, and those two facts explain almost everything about how it feels to write Ruby code. Instead of compiling your program into a standalone executable ahead of time, the ruby command reads your source, translates it into instructions, and starts running it right away — which is why you can drop into irb and get instant feedback on a single line. And because literally every value you touch, from an integer to nil to a class itself, is an object that responds to messages (methods), the same core mechanic — sending a message to a receiver and letting Ruby find the matching method — is how you build everything else in the language. Once these two ideas click, the rest of Ruby’s syntax stops looking like a pile of special cases and starts looking like one consistent system.

Overview: How Ruby Works

“Interpreted” used to mean a program was read and executed line by line, with no separate build step producing machine code. Ruby’s default implementation, CRuby (also called MRI, Matz’s Ruby Interpreter), is a bit more sophisticated than that: when you run ruby script.rb, it first parses your source into an abstract syntax tree, then compiles that tree into an internal bytecode format called YARV instructions, and finally executes those instructions on a virtual machine. All of that happens automatically and invisibly the moment you run the file — there is no separate compile-then-link workflow exposed to you, no .exe or .class file left behind. That is what people mean when they call Ruby an interpreted language: you write source, you run source, and Ruby handles translation internally on every run.

Execution then proceeds top to bottom through the file. Method and class definitions are themselves executable statements — def and class run when Ruby reaches them, registering the method or class so it can be looked up later. This is why a method must be defined before it is called, but the call itself can be anywhere textually, including inside another method defined earlier, because Ruby resolves method calls at the moment they run, not while parsing.

The other half of the story is Ruby’s object model. Every piece of data is an instance of some class, including things other languages treat as primitives:

Expression Value it produces
42.class Integer
nil.class NilClass
true.class TrueClass
Integer.class Class

Because everything is an object, calling a method is really sending a message to a receiver. Ruby looks for a matching method by walking the receiver’s ancestor chain: first the object’s own singleton class (methods defined on that one object), then its class, then any modules mixed in with include (searched in reverse order of inclusion, most recently included first), then the superclass and its included modules, and so on up to Object, Kernel, and finally BasicObject. The first matching method found wins. If the search reaches the top with nothing found, Ruby does not simply give up — it calls method_missing on the original receiver, which by default raises NoMethodError but which you can override to handle arbitrary method names dynamically.

Truthiness follows the same “everything is an object” logic taken to its natural conclusion: in an if, unless, or &&/|| expression, only nil and false are falsy. Every other object — including 0, an empty string "", and an empty array [] — is truthy. This trips up people coming from languages where 0 or "" is falsy, and it is one of the most common sources of subtle Ruby bugs.

Syntax

There is no single “how Ruby works” syntax, but the general shape of defining and using an object looks like this:

class ClassName
  def method_name(param1, param2 = "default", *rest, keyword:, keyword_opt: nil)
    # method body
    # the value of the LAST evaluated expression is returned automatically
  end
end

object = ClassName.new
object.method_name(value1, value2, keyword: value3)
Part Meaning
class ClassName ... end Defines a new class; runs once, registering the class and its methods.
def method_name ... end Defines an instance method; the body executes each time the method is called, not when it is defined.
param2 = "default" An optional parameter with a default value.
*rest Collects any extra positional arguments into an array.
keyword: / keyword_opt: nil Required and optional keyword arguments — preferred for methods with several parameters, since call sites stay self-documenting.
ClassName.new Sends the new message to the class object, which allocates an instance and calls initialize on it.
object.method_name(...) Sends a message (method call) to object; Ruby searches the ancestor chain to find the method to run.

Examples

Example 1: Everything really is an object

puts 42.class
puts 42.class.class
puts nil.class
puts true.class
puts "hello".class

range = (1..5)
puts range.class

if 1
  puts "1 is truthy"
end

if ""
  puts "empty string is truthy"
end

if []
  puts "empty array is truthy"
end

if nil
  puts "nil is truthy"
else
  puts "nil is falsy"
end

Output:

Integer
Class
NilClass
TrueClass
String
Range
1 is truthy
empty string is truthy
empty array is truthy
nil is falsy

Every value responds to .class, including a class itself — 42.class is Integer, and Integer is in turn an instance of Class. The four if checks show Ruby’s truthiness rule in action: 1, "", and [] all count as truthy because they are not nil or false; only the final check, on literal nil, takes the else branch.

Example 2: Method lookup and the ancestor chain

module Greetable
  def greet
    "Hello, I am #{name}"
  end
end

class Person
  include Greetable

  attr_reader :name

  def initialize(name)
    @name = name
  end
end

person = Person.new("Ada")
puts person.greet
puts Person.ancestors.inspect

Output:

Hello, I am Ada
[Person, Greetable, Object, Kernel, BasicObject]

Person does not define greet itself — it comes from the Greetable module mixed in with include. When person.greet is called, Ruby walks the ancestor chain shown by Person.ancestors: it checks Person first (no greet there), then Greetable (found), and stops. Inside greet, calling name works because it is looked up on self (the Person instance), which does have it via attr_reader.

Example 3: method_missing and dynamic dispatch

class GhostResponder
  def method_missing(name, *args)
    if name.to_s.start_with?("ask_")
      topic = name.to_s.sub("ask_", "")
      "You asked about #{topic} with #{args.inspect}"
    else
      super
    end
  end

  def respond_to_missing?(name, include_private = false)
    name.to_s.start_with?("ask_") || super
  end
end

ghost = GhostResponder.new
puts ghost.ask_weather("today")
puts ghost.respond_to?(:ask_weather)

Output:

You asked about weather with ["today"]
true

Calling ghost.ask_weather("today") sends a message Ruby cannot find anywhere in the ancestor chain, so it falls back to method_missing, which inspects the requested method name and args and builds a response. Overriding respond_to_missing? alongside method_missing keeps introspection methods like respond_to? honest about what the object can actually handle — skipping it is a common mistake when writing dynamic Ruby code.

How It Works Step by Step

When you run a Ruby file, roughly this happens:

  • Ruby parses the source text into an abstract syntax tree, checking for syntax errors.
  • CRuby compiles that tree into internal YARV bytecode instructions.
  • The virtual machine executes those instructions from top to bottom; top-level statements, class bodies, and def statements all run in the order Ruby encounters them.
  • Whenever a method call is reached, Ruby evaluates the receiver and arguments, then searches the receiver’s singleton class, its class, included modules (most recently included first), and superclasses in order until it finds a matching method name.
  • If a match is found, its body executes; the value of the last expression evaluated in that body becomes the method’s return value automatically, with no return keyword required.
  • If no match is found anywhere in the chain, Ruby calls method_missing on the receiver; the default implementation (inherited from BasicObject) raises NoMethodError.

Common Mistakes

Mistake 1: Treating 0 or an empty collection as falsy

inventory_count = 0

if inventory_count
  puts "Item is in stock"
else
  puts "Item is out of stock"
end

This prints Item is in stock, which is wrong for an inventory of zero. In Ruby only nil and false are falsy, so 0 passes the if check just like any other object. The fix is to test the actual condition you mean, not the object’s truthiness:

inventory_count = 0

if inventory_count.zero?
  puts "Item is out of stock"
else
  puts "Item is in stock"
end

Output:

Item is out of stock

Mistake 2: Leaking a mutable reference to internal state

class Cart
  def initialize
    @items = []
  end

  def items
    @items
  end
end

cart = Cart.new
cart.items << "apple"

receipt_preview = cart.items
receipt_preview << "gift wrap"

puts cart.items.inspect

This prints [“apple”, “gift wrap”]. The items getter hands back the exact same Array object stored in @items, not a copy, so appending to receipt_preview silently mutates the cart too. Returning a duplicate (and adding a proper writer method) fixes it:

class Cart
  def initialize
    @items = []
  end

  def items
    @items.dup
  end

  def add_item(item)
    @items << item
  end
end

cart = Cart.new
cart.add_item("apple")

receipt_preview = cart.items
receipt_preview << "gift wrap"

puts cart.items.inspect

Output:

["apple"]

Mistake 3: Chaining onto a bang method that can return nil

words = ["ruby", "gem", "rails"]
result = words.uniq!.length
puts result

Several bang methods, including uniq!, compact!, and reject!, return nil (instead of self) whenever they made no change. Here there were no duplicates to remove, so words.uniq! returns nil, and calling .length on nil raises NoMethodError: undefined method 'length' for nil. Use the non-mutating version when you just want a result, and keep the two steps separate:

words = ["ruby", "gem", "rails"]
unique_words = words.uniq
puts unique_words.length

Output:

3

Best Practices

  • Test conditions explicitly (count.zero?, array.empty?, value.nil?) instead of relying on an object’s bare truthiness when you mean something more specific than “not nil/false.”
  • Know which bang methods return nil on no change (uniq!, compact!, reject!, select!, and similar) and avoid chaining directly off them.
  • Return a dup or clone of internal arrays and hashes from getters when callers should not be able to mutate your object’s state.
  • Pair method_missing with a matching respond_to_missing? so introspection (respond_to?, method) stays accurate.
  • When debugging “which method actually ran,” check SomeClass.ancestors or object.method(:name).owner rather than guessing.
  • Prefer keyword arguments for methods that take more than one or two parameters so call sites stay self-explanatory.

Practice Exercises

  • Write a script that prints the .class of five different values (an integer, a float, a string, a symbol, and nil) and confirms that each result is itself an object by calling .class on the class.
  • Define a class Wallet with a balance getter backed by an internal integer. Write a small script that shows why if wallet.balance alone is not a safe way to check for “no money left,” even when the balance is 0.
  • Create two modules, Flyable and Swimmable, each defining a differently named method, and mix both into a Duck class. Print Duck.ancestors and confirm the module you included last appears closer to the front of the chain than the one included first.

Summary

  • Ruby is interpreted: CRuby parses your source, compiles it internally to YARV bytecode, and executes it immediately — there is no separate build step you manage.
  • Execution runs top to bottom; def and class are themselves statements that register methods and classes when Ruby reaches them.
  • Everything is an object, including integers, nil, true/false, and classes themselves — every one of them responds to .class.
  • Only nil and false are falsy; 0, "", and [] are all truthy, a frequent source of bugs for newcomers.
  • Calling a method sends a message that Ruby resolves by walking the ancestor chain: singleton class, class, included modules, superclasses, up to BasicObject.
  • When no method matches, Ruby calls method_missing, which by default raises NoMethodError but can be overridden for dynamic dispatch.
  • A method’s return value is whatever its last expression evaluates to — watch for that when a bang method can return nil or when a getter leaks a mutable reference.