Ruby โ Complete Beginner Tutorial
6 modules ยท 18 examples ยท Click โถ Run Code to see the console output
๐ก How to use this tutorial
Read each explanation, study the code, then click โถ Run Code to see the output.
Variables & Basics
Ruby is one of the most beginner-friendly languages ever created. There are no semicolons, no curly braces, and no type declarations โ you just write what you want to happen and Ruby does it. Variables start with a lowercase letter. puts prints to the screen with a new line. print prints without a new line. String interpolation with #{} lets you embed any expression directly into a string.
Variables and Output
Ruby variables are simple โ just write a name, an equals sign, and the value. No var, let, or type needed. puts (put string) prints to the screen and adds a new line. print does the same but without the new line. p prints the raw value including quotes โ useful for debugging.
# Variables โ no keywords needed
name = "Alice"
age = 25
height = 1.72
is_cool = true
# puts โ print with a new line
puts "Hello, #{name}!"
puts "Age: #{age}"
puts "Height: #{height}m"
puts "Cool? #{is_cool}"
# print โ no new line at end
print "First "
print "Second "
print "Third"
puts # just a blank line
# p โ shows the raw value (great for debugging)
p name
p age
p [1, 2, 3]
# Multiple assignment
x, y, z = 10, 20, 30
puts "#{x}, #{y}, #{z}"In Ruby, the # symbol starts a comment โ everything after it on that line is ignored. Use comments to explain why you wrote something, not just what it does. Ruby developers also use # for string interpolation inside double-quoted strings: "Hello, #{name}".
Strings and Their Methods
In Ruby, strings are objects with built-in methods. You call methods by putting a dot after the string followed by the method name. This makes string manipulation feel natural and readable. Ruby has dozens of built-in string methods โ here are the ones you will use most often.
greeting = "Hello, World!"
# Case methods
puts greeting.upcase # ALL CAPS
puts greeting.downcase # all lower
puts greeting.swapcase # sWAP cASE
puts greeting.capitalize # First letter up
# Checking and searching
puts greeting.length # character count
puts greeting.include?("World") # true/false
puts greeting.start_with?("Hello")
puts greeting.end_with?("!")
# Modifying
puts greeting.reverse
puts greeting.gsub("World", "Ruby") # replace all
puts greeting.sub("Hello", "Hi") # replace first
# Trimming and splitting
messy = " too much space "
puts messy.strip # remove leading/trailing spaces
puts messy.lstrip # remove left only
sentence = "one two three"
words = sentence.split(" ")
puts words.inspect
puts words.join(", ")Notice that methods ending with ? return true or false โ this is a Ruby convention. Methods ending with ! usually modify the object in place (mutate it) rather than returning a new value. For example, str.upcase returns a new string, but str.upcase! changes str itself.
Numbers and Maths
Ruby has Integer (whole numbers) and Float (decimal numbers). Maths operators work as expected. Integer division drops the decimal โ 7 / 2 is 3, not 3.5. Use a Float in the division to get a decimal result. The ** operator raises to a power. % is modulo (the remainder after division).
# Integer maths
puts 10 + 3 # 13
puts 10 - 3 # 7
puts 10 * 3 # 30
puts 10 / 3 # 3 (integer division โ drops decimal!)
puts 10 % 3 # 1 (remainder)
puts 10 ** 3 # 1000 (power)
# Float maths โ keeps the decimal
puts 10.0 / 3 # 3.3333...
puts 7.5 + 2.5
# Conversion methods
puts 42.to_f # convert integer to float -> 42.0
puts 3.9.to_i # convert float to integer -> 3 (truncates!)
puts "25".to_i # convert string to integer -> 25
puts "3.14".to_f
# Useful number methods
puts -15.abs # absolute value -> 15
puts 3.14159.round(2) # round to 2 decimal places -> 3.14
puts 9.7.ceil # round up -> 10
puts 9.2.floor # round down -> 9
# Random number
puts rand(100) # random 0 to 99The difference between 10 / 3 (gives 3) and 10.0 / 3 (gives 3.333) catches many beginners off guard. Ruby performs integer division when both operands are integers. Simply make one of them a float โ either write it as 10.0, or call .to_f on it โ to get the decimal result.
Control Flow
Ruby has all the standard control flow structures, but with a clean twist โ you can put conditions at the end of a line (called a "trailing conditional"). This often makes code read more like English. Ruby uses elsif (not else if or elseif) for additional branches. The unless keyword is the opposite of if โ it runs when the condition is false.
if, elsif, else and unless
The if statement runs code when a condition is true. elsif adds more branches. else catches everything that did not match. unless is like if but inverted โ it runs when the condition is false. Trailing conditions let you write a one-liner: puts "hello" if name == "Alice".
score = 75
# Standard if / elsif / else
if score >= 90
puts "Grade: A โ Excellent!"
elsif score >= 75
puts "Grade: B โ Good job!"
elsif score >= 60
puts "Grade: C โ Average"
elsif score >= 50
puts "Grade: D โ Just passed"
else
puts "Grade: F โ Try again"
end
# unless โ runs when condition is FALSE
temperature = 8
unless temperature > 20
puts "It's cold today โ bring a jacket!"
end
# Trailing conditions โ very Ruby-ish
age = 17
puts "You can vote!" if age >= 18
puts "You cannot vote yet." unless age >= 18
# Ternary operator โ one-line if/else
status = age >= 18 ? "adult" : "minor"
puts "Status: #{status}"The trailing if and unless (called "postfix conditionals") are one of Ruby's most beloved features. puts "done" if success.zero? reads almost like a sentence. Use this style for simple one-liners. For multi-line logic, use the standard block form with end.
case / when โ Pattern Matching
Ruby's case/when is more powerful than a simple switch statement. It can match against values, ranges, regular expressions, and even classes. When no condition matches, the else block runs. The case expression returns a value, so you can assign it directly to a variable.
# Basic case / when
day = "Wednesday"
case day
when "Monday"
puts "Start of the week โ let's go!"
when "Tuesday"
puts "Tuesday energy!"
when "Wednesday"
puts "Midweek already!"
when "Thursday"
puts "Almost Friday..."
when "Friday"
puts "TGIF!"
when "Saturday", "Sunday"
puts "Weekend โ rest and recharge!"
else
puts "Not a valid day"
end
# case with ranges
age = 22
category = case age
when 0..12 then "Child"
when 13..17 then "Teenager"
when 18..64 then "Adult"
when 65.. then "Senior"
end
puts "Age category: #{category}"
# case matching on type (class)
value = 3.14
case value
when Integer
puts "It's an integer"
when Float
puts "It's a float"
when String
puts "It's a string"
endRuby case/when uses the === operator (triple equals) to test each condition. This is why it can match ranges (18..64 === 22 returns true), classes (Integer === 42 returns true), and regex patterns. This is far more flexible than switch statements in other languages.
Loops & Iterators
Ruby has traditional loops (while, until, loop) but the Ruby way to repeat things is through iterators โ methods that take a block of code and run it multiple times. The most common are times (repeat N times), each (loop over a collection), map (transform a collection), select (filter a collection), and reduce (combine values into one). Blocks are the curly braces or do...end that you pass to these methods.
times and each
The times method is the simplest way to repeat code. Call it on any integer. The each method is how you loop over an array or range โ it gives you each element one at a time inside the block variable (the name between the | pipes). Blocks can be written with {} for one-liners or do...end for multiple lines.
# times โ repeat exactly N times
5.times do |i|
puts "Iteration #{i}" # i goes 0, 1, 2, 3, 4
end
puts "---"
# upto and downto
1.upto(5) { |n| print "#{n} " }
puts
5.downto(1) { |n| print "#{n} " }
puts
puts "---"
# each on an array
fruits = ["apple", "banana", "cherry", "mango"]
fruits.each do |fruit|
puts "I like #{fruit}"
end
puts "---"
# each on a range
(1..5).each { |n| puts n * n } # squares
puts "---"
# each_with_index โ get the element AND its index
fruits.each_with_index do |fruit, index|
puts "#{index + 1}. #{fruit.capitalize}"
endThe variable inside the pipes |fruit| is called the block parameter. You can name it anything descriptive. Convention is to use the singular of the collection name โ if your collection is called users, name the block param user. If it is numbers, name it num or number.
map, select and reduce
These three methods are the most powerful iterators in Ruby. map transforms every element and returns a new array. select keeps only elements where the block returns true. reject is the opposite of select. reduce combines all elements into a single value using an accumulator.
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
# map โ transform every element
doubled = numbers.map { |n| n * 2 }
puts "Doubled: #{doubled.inspect}"
squared = numbers.map { |n| n ** 2 }
puts "Squared: #{squared.inspect}"
# select โ keep only matching elements
evens = numbers.select { |n| n.even? }
puts "Evens: #{evens.inspect}"
over_five = numbers.select { |n| n > 5 }
puts "Over 5: #{over_five.inspect}"
# reject โ remove matching elements (opposite of select)
odds = numbers.reject { |n| n.even? }
puts "Odds: #{odds.inspect}"
# reduce / inject โ combine all into one value
total = numbers.reduce(0) { |sum, n| sum + n }
puts "Total: #{total}"
product = numbers.reduce(1) { |prod, n| prod * n }
puts "Product: #{product}"
# Method shorthand โ even cleaner!
puts numbers.sum # built-in sum
puts numbers.min
puts numbers.max
puts numbers.sort.inspectYou can chain these methods together. numbers.select { |n| n.even? }.map { |n| n * 10 } first keeps only even numbers, then multiplies each by 10. This chaining style is one of the most elegant things about Ruby and is used constantly in real code.
Methods
In Ruby, functions are called methods and are defined with the def keyword. Every method in Ruby returns a value โ the last evaluated expression is the implicit return value (you do not need to write return unless you want to exit early). Methods can have positional arguments, default values, and keyword arguments. Question mark methods (?) return booleans. Bang methods (!) modify in place.
Defining and Calling Methods
Use def to start a method and end to close it. Call a method by just writing its name. Ruby does not require parentheses when calling methods without arguments. The last expression in the method is automatically returned โ no return keyword needed.
# Simple method โ no parameters
def say_hello
"Hello, World!" # this is returned automatically
end
puts say_hello
# Method with parameters
def greet(name, greeting = "Hello")
"#{greeting}, #{name}!"
end
puts greet("Alice")
puts greet("Bob", "Good morning")
# Multiple return values (returns an array)
def min_and_max(numbers)
[numbers.min, numbers.max]
end
low, high = min_and_max([3, 1, 9, 2, 7])
puts "Min: #{low}, Max: #{high}"
# Method with a question mark โ returns boolean
def adult?(age)
age >= 18
end
puts adult?(20)
puts adult?(15)
# Calculating with a method
def bmi(weight_kg, height_m)
(weight_kg / height_m ** 2).round(1)
end
puts "BMI: #{bmi(70, 1.75)}"The implicit return (last expression is returned automatically) is one of Ruby's nicest features. You will quickly stop writing return and rely on this. The only time to use explicit return is when you want to exit a method early โ for example inside an if statement before reaching the end.
Keyword Arguments and Blocks
Keyword arguments let you pass arguments by name, making method calls much clearer. You can also define methods that accept a block using yield โ this is how Ruby's iterators like each and map work internally.
# Keyword arguments
def create_user(name:, age:, role: "member")
"#{role.upcase}: #{name} (#{age})"
end
puts create_user(name: "Alice", age: 25)
puts create_user(name: "Bob", age: 30, role: "admin")
# Order does not matter with keyword args!
puts create_user(age: 22, name: "Carol")
# Method that accepts a block with yield
def repeat(times)
times.times { yield }
end
repeat(3) { puts "Ruby is great!" }
# Block with a value passed to it
def transform(value)
result = yield(value)
"Transformed: #{result}"
end
puts transform(5) { |n| n * 10 }
puts transform("hello") { |s| s.upcase }
# Check if a block was given
def optional_block(value)
if block_given?
yield(value)
else
value
end
end
puts optional_block(10) { |n| n + 5 }
puts optional_block(10)Keyword arguments (name: value) are preferred over positional arguments when a method has more than two or three parameters. They make the call site self-documenting โ create_user(name: "Alice", age: 25) is much clearer than create_user("Alice", 25) where you have to check the method definition to know the order.
Classes & Objects
Everything in Ruby is an object โ even integers and strings. You can create your own object types using class. The initialize method runs when you create a new object with ClassName.new. Instance variables (starting with @) belong to that specific object. attr_accessor is a shortcut that automatically creates getter and setter methods for your instance variables.
Creating Classes
Define a class with the class keyword. The initialize method is the constructor โ it runs when you call ClassName.new. Instance variables start with @. attr_reader gives read-only access. attr_writer gives write-only access. attr_accessor gives both. Methods defined inside the class become the object's behaviour.
class Person
# Creates getter and setter for name and age
attr_accessor :name, :age
# initialize runs when Person.new is called
def initialize(name, age)
@name = name # @ means instance variable
@age = age
end
def introduce
"Hi! I'm #{@name} and I'm #{@age} years old."
end
def birthday!
@age += 1
puts "Happy birthday #{@name}! Now #{@age}."
end
def adult?
@age >= 18
end
# to_s โ called when object is converted to string
def to_s
"Person(#{@name}, #{@age})"
end
end
# Create objects
alice = Person.new("Alice", 25)
bob = Person.new("Bob", 16)
puts alice.introduce
puts bob.introduce
# Using attr_accessor
puts alice.name # getter
alice.name = "Alice Smith" # setter
puts alice.name
puts alice.adult?
puts bob.adult?
alice.birthday!
puts alice # calls to_s automaticallyattr_accessor :name creates two methods behind the scenes: a getter (def name; @name; end) and a setter (def name=(value); @name = value; end). This is called metaprogramming โ Ruby generating code for you. It is one of the most powerful and elegant features of the language.
Inheritance with <
Use < to inherit from another class. The child class gets all the parent's methods automatically. Override a parent method by defining it again in the child class. Call the parent's version of a method using super. Class variables (@@) are shared across all instances of a class and its subclasses.
class Animal
attr_reader :name
def initialize(name)
@name = name
end
def speak
"..."
end
def describe
"#{@name} says: #{speak}"
end
end
# Dog inherits from Animal using <
class Dog < Animal
def speak
"Woof!"
end
def fetch(item)
"#{@name} fetches the #{item}!"
end
end
class Cat < Animal
def speak
"Meow!"
end
def purr
"Purrrrr..."
end
end
class Duck < Animal
def speak
"Quack!"
end
end
# Create objects
dog = Dog.new("Buddy")
cat = Cat.new("Whiskers")
duck = Duck.new("Donald")
# describe is inherited from Animal
puts dog.describe
puts cat.describe
puts duck.describe
# Methods specific to each class
puts dog.fetch("ball")
puts cat.purr
# Polymorphism โ same method, different behaviour
animals = [dog, cat, duck]
animals.each { |a| puts a.speak }The last three lines demonstrate polymorphism โ calling the same method (speak) on different types of objects and getting different results. This is one of the key benefits of inheritance. The animals array can hold Dogs, Cats, and Ducks and treat them all the same through the Animal interface.
Arrays & Hashes
Arrays hold ordered lists of items (indexed from 0). Hashes hold key-value pairs โ like a dictionary. Ruby arrays and hashes have dozens of built-in methods that make working with collections elegant and concise. In modern Ruby, hash keys are usually symbols (:name) rather than strings ("name") โ symbols are faster and use less memory.
Arrays โ Ordered Lists
Arrays are created with square brackets. Access elements by index (starting from 0). Negative indexes count from the end (-1 is the last element). Ruby arrays have methods for adding, removing, searching, sorting, and transforming data built right in.
# Creating arrays
fruits = ["apple", "banana", "cherry"]
numbers = [3, 1, 4, 1, 5, 9, 2, 6]
mixed = [1, "hello", true, 3.14]
# Accessing elements
puts fruits[0] # first element
puts fruits[-1] # last element
puts fruits.first
puts fruits.last
puts fruits[1, 2].inspect # 2 elements starting at index 1
# Adding and removing
fruits.push("mango") # add to end
fruits << "grape" # same as push
fruits.unshift("avocado") # add to beginning
puts fruits.inspect
removed = fruits.pop # remove from end
puts "Removed: #{removed}"
puts fruits.inspect
# Finding and checking
puts fruits.include?("banana")
puts fruits.index("cherry")
puts fruits.length
# Sorting and transforming
puts numbers.sort.inspect
puts numbers.sort.reverse.inspect
puts numbers.uniq.inspect # remove duplicates
puts numbers.min
puts numbers.max
puts numbers.sumThe << operator (shovel operator) for adding to arrays is very Ruby. You will see it everywhere โ array << item, string << "more text". It is faster than + for adding to arrays because it modifies the array in place rather than creating a new one.
Hashes โ Key-Value Pairs
Hashes store data as key-value pairs. Modern Ruby uses symbols as keys (:name) which is faster and cleaner. Access a value with hash[:key]. Use each to iterate over all pairs. Hashes have many useful methods like keys, values, merge, and select.
# Creating a hash with symbol keys
person = {
name: "Alice",
age: 25,
city: "Cape Town",
skill: "Ruby"
}
# Accessing values
puts person[:name]
puts person[:age]
puts person[:city]
# Adding and updating
person[:email] = "alice@example.com"
person[:age] = 26 # update existing
puts person.inspect
# Checking
puts person.key?(:name)
puts person.key?(:phone)
puts person.value?("Alice")
# Iterating
person.each do |key, value|
puts " #{key}: #{value}"
end
# Useful methods
puts person.keys.inspect
puts person.values.inspect
puts person.length
# Merge two hashes
defaults = { role: "user", active: true }
full = defaults.merge(person)
puts "Role: #{full[:role]}"
# select / reject on hashes
young_fields = person.select { |k, v| v.is_a?(Integer) }
puts young_fields.inspectWhen you define a hash with symbol keys you can use either { name: "Alice" } (new style) or { :name => "Alice" } (old style with hash rocket). The new style (name:) is preferred in modern Ruby because it is shorter and easier to read. Both work identically.
You finished the Ruby tutorial!
You can now write Ruby with variables, control flow, loops, iterators, methods, classes and collections. You have the foundation to start learning Ruby on Rails and build real web applications.