Ruby Introduction
Ruby is a dynamic, object-oriented programming language created by Yukihiro "Matz" Matsumoto in the mid-1990s around a single guiding idea: programming should make developers happy, not fight them. It blends the flexibility of scripting languages with an elegant, readable syntax that often reads like plain English, while remaining powerful enough to run large production systems (most famously the Ruby on Rails web framework). This lesson introduces what Ruby actually is, how it executes your code under the hood, and gives you enough hands-on practice to start writing real Ruby programs today.
Overview: What Ruby Is and How It Works
Ruby is an interpreted, dynamically-typed, object-oriented language. When you run ruby myfile.rb, Ruby’s default implementation (called CRuby or MRI, short for Matz’s Ruby Interpreter) reads your entire file, checks that its syntax is valid, compiles it internally into bytecode for a virtual machine called YARV, and then executes that bytecode from top to bottom. You do not need to manage this process yourself — from your point of view, Ruby simply runs your file one statement at a time.
The single most important idea in Ruby is that everything is an object. Not just the things you create with class, but integers, strings, true, false, and even nil (Ruby’s version of "nothing") are full objects with a class and methods you can call on them. Typing 1.class in Ruby returns Integer, and nil.class returns NilClass. This is why you can chain method calls like "hello".upcase.reverse without any special syntax — you are simply calling methods on objects, the same as everywhere else in the language.
A closely related idea is Ruby’s truthiness rule, and it trips up almost everyone coming from another language: in an if check, only nil and false are falsy. Every other value — including 0, an empty string "", and an empty array [] — is truthy. In languages like Python or JavaScript those values are often falsy, so carrying that assumption into Ruby is a common source of bugs.
Ruby is also dynamically typed: a variable is just a name bound to whatever object it currently points at, and you can reassign it to a completely different kind of object at any time. There is no compile-time type checking; type-related mistakes surface only when the offending line actually runs.
Blocks, Procs, and Lambdas (a first look)
You will quickly notice Ruby code passing a chunk of logic to a method using do ... end or curly braces, for example [1, 2, 3].each { |n| puts n }. That chunk is called a block, and importantly a block is not itself an object — you cannot store it in a variable on its own. When you do need an object you can save and pass around, Ruby gives you Proc.new (or the proc method) and lambda. Both wrap code into a callable object, but they differ in two important ways: a lambda checks its argument count strictly and raises an error on a mismatch, while a proc is lenient and just fills missing arguments with nil; and a bare return inside a lambda only exits the lambda, while a return inside a proc exits the method that the proc was created in. Later lessons in this course cover blocks, procs, and lambdas in full — for now, just recognize the do...end/{...} syntax as "a piece of code handed to a method."
How Ruby Finds a Method (the ancestor chain)
When you call a method on an object, Ruby does not just check that object’s class in isolation. It walks an ordered list called the ancestor chain: first the object’s own singleton class, then its class, then any modules mixed in with include (checked in reverse order of inclusion), then the superclass and its mixed-in modules, and so on up to Object and finally BasicObject. The first matching method definition found wins. If nothing matches, Ruby calls a special method named method_missing, which by default raises a NoMethodError but can be overridden to handle calls dynamically. You do not need to memorize this today, but it is the mechanism that makes both inheritance and mixins work, and it explains error messages you will see constantly as you learn.
Syntax
The example below shows several core pieces of Ruby syntax together: variable assignment, a regular method definition, an endless method (Ruby 3.0+), string interpolation, and a conditional.
| Piece | Meaning |
|---|---|
name = "Alice" |
Variable assignment; no type declaration needed |
def greet(name) ... end |
A method definition; the last evaluated expression is returned automatically |
def greet_short(name) = "Hi" |
An endless method: a one-line method body after =, no end needed |
"Hello, #{name}!" |
String interpolation; embeds the result of any expression into a string |
if ... else ... end |
A conditional; in Ruby, if is itself an expression that evaluates to a value |
# a comment |
Single-line comment; everything after # on that line is ignored |
name = "Alice"
def greet(name)
"Hello, #{name}!"
end
def greet_short(name) = "Hi, #{name}!"
if name.empty?
puts "No name given"
else
puts greet(name)
end
Output:
Hello, Alice!
Examples
Example 1: Variables, Interpolation, and Objects
Every value in Ruby carries its class with it, which you can inspect at any time by calling .class.
name = "Ruby"
version = 3.2
puts "Hello, #{name}!"
puts "You're learning version #{version}"
puts name.class
puts version.class
puts name.upcase
Output:
Hello, Ruby!
You're learning version 3.2
String
Float
RUBY
The interpolated strings insert the current value of name and version directly into the output. name.class reports String and version.class reports Float, confirming that these are ordinary objects. name.upcase returns a new, all-uppercase string without modifying name itself, since upcase (no exclamation point) never mutates its receiver.
Example 2: Methods, Endless Methods, and Truthiness
This example defines a one-line endless method and then walks through Ruby’s truthiness rule with several values that are truthy in Ruby but might be falsy in other languages.
def square(x) = x * x
def describe(value)
if value
"truthy"
else
"falsy"
end
end
puts square(5)
puts describe(0)
puts describe("")
puts describe([])
puts describe(nil)
puts describe(false)
Output:
25
truthy
truthy
truthy
falsy
falsy
square(5) returns 25 because an endless method’s body is just an expression whose value is returned automatically. The four calls to describe show the truthiness rule in action: 0, the empty string "", and the empty array [] all evaluate as truthy in an if, so only nil and false reach the else branch.
Example 3: A Hash, a Block, and Symbols
Real Ruby code constantly combines collections with blocks. This example builds an inventory hash, sums its values, and prints each entry.
inventory = {
apples: 10,
bananas: 5,
cherries: 20
}
total = inventory.values.reduce(0) { |sum, count| sum + count }
inventory.each do |fruit, count|
puts "#{fruit}: #{count}"
end
puts "Total items: #{total}"
puts inventory.class
puts 42.class
puts nil.class
puts((5 > 3).class)
Output:
apples: 10
bananas: 5
cherries: 20
Total items: 35
Hash
Integer
NilClass
TrueClass
The hash keys — apples, bananas, cherries — are written as symbols (:apples), not strings. A symbol is an immutable, interned identifier: every time your program writes :apples, Ruby hands back the exact same object in memory, whereas two separate string literals "apples" are two distinct objects that merely contain equal characters. Because symbols are cheap to compare and cannot be mutated, they are the idiomatic choice for hash keys and internal identifiers, while strings remain the right choice for text a user will actually see. Ruby hashes also preserve insertion order, which is why each prints the fruits in the order they were defined. Finally, note that even a comparison like 5 > 3 produces a real object — here, the singleton object true, whose class is TrueClass.
How It Works Step by Step
Take Example 2 above and trace it the way the interpreter does:
- Ruby first reads and parses the entire file, verifying the syntax is valid before running anything.
- The two
defstatements are executed first, but executing adefonly defines the method and stores it on the current class — it does not run the method body yet. - Execution then proceeds top to bottom.
puts square(5)callssquare, which evaluatesx * x(25) and, because it has no explicitreturn, automatically returns the value of that last (and only) expression. - Each subsequent line calls
describewith a different value. Insidedescribe, theif valuecheck evaluatesvaluefor truthiness; the branch taken determines whether the string"truthy"or"falsy"is the last expression evaluated, and that string becomes the method’s return value. putsconverts whatever object it receives to a string (viato_s) and writes it to standard output followed by a newline.putsitself always returnsnil, which matters if you ever try to use its result.
Common Mistakes
Mistake 1: Forgetting a Method’s Return Value Is Whatever It Last Evaluated
Because Ruby methods return their last evaluated expression automatically, ending a method with a call like puts silently makes the method return nil — not the value you just printed.
def add_and_log(a, b)
result = a + b
puts "Sum is #{result}"
end
value = add_and_log(2, 3)
puts value.inspect
Output:
Sum is 5
nil
The method’s last line is puts "Sum is #{result}", and puts always returns nil, so value ends up nil even though the sum was computed correctly. Add the value you actually want returned as the final expression:
def add_and_log(a, b)
result = a + b
puts "Sum is #{result}"
result
end
value = add_and_log(2, 3)
puts value.inspect
Output:
Sum is 5
5
Mistake 2: Mutating an Object Through a Shared Reference
Assigning one variable to another does not copy the underlying object — both names point at the same object in memory, so mutating one mutates both.
original = [1, 2, 3]
copy = original
copy << 4
puts original.inspect
Output:
[1, 2, 3, 4]
copy = original does not create a new array; it just gives a second name to the same array object, so appending to copy with << also changes what original sees. To get an independent copy, call .dup (or .clone):
original = [1, 2, 3]
copy = original.dup
copy << 4
puts original.inspect
puts copy.inspect
Output:
[1, 2, 3]
[1, 2, 3, 4]
The same footgun shows up with bang methods like sort!: it mutates the receiver in place and returns nil if nothing changed, so chaining another method call directly after sort! can unexpectedly call that method on nil.
Best Practices
- Use
snake_casefor method and variable names, andCamelCasefor classes and modules — this is the community-wide style, and tools like RuboCop enforce it by default. - Prefer endless methods (
def square(x) = x * x) for short, single-expression methods, and a regulardef...endbody for anything with multiple statements. - Reach for
each,map,select, andreduceinstead of manual index-based loops; they are more idiomatic and communicate intent more clearly. - Use symbols (
:status) for hash keys and internal identifiers, and reserve strings for text meant to be displayed or manipulated as text. - Remember that only
nilandfalseare falsy — never assume0,"", or[]behave like they might in another language. - Call
.dupor.clonewhen you need an independent copy of a mutable object such as an array or hash. - Use bang methods (
sort!,map!) only when you intend to mutate the receiver, and check their return value carefully since some returnnilwhen nothing changed. - Fire up
irb(Ruby's interactive shell) whenever you are unsure what a method returns — experimenting there is faster than guessing.
Practice Exercises
- Write a method
fahrenheit_to_celsius(f)that returns(f - 32) * 5.0 / 9. Call it with98.6and print the result. Expected output is approximately37.0. - Given
scores = [55, 82, 91, 40, 76], useselectto keep only the scores that are 60 or above, then print each remaining score on its own line usingeach. You should see three lines printed:82,91, and76. - Without running any code, predict what
[].class,"".class,nil.class, and0.classeach return, and state which of those four values would fail anifcheck (be falsy). Then check your answer withirb.
Summary
- Ruby is an interpreted, dynamically-typed, object-oriented language built around the idea of programmer happiness.
- Everything in Ruby is an object, including integers, strings,
true,false, andnil— every value has a class and responds to methods. - Only
nilandfalseare falsy in a conditional;0,"", and[]are all truthy. - Methods automatically return their last evaluated expression; endless methods (
def name(args) = expr) are the idiomatic form for one-liners. - Method calls are resolved by walking an ancestor chain of singleton class, class, mixed-in modules, and superclasses, which is what makes inheritance and mixins work.
- Symbols are immutable, interned identifiers best used for hash keys and internal identifiers; strings are for actual text.
- Assigning a variable to another variable shares the same object reference; use
.dupor.clonewhen you need an independent copy.
