Ruby interview questions focus on language fundamentals, object-oriented programming, data structures, algorithms, and common libraries. You can expect questions about Ruby syntax, blocks, modules, and performance optimization.
Did you know that Ruby on Rails powers over a million websites, including platforms like Shopify and GitHub? This robust framework, combined with Ruby’s simplicity and elegance, has made it a favorite among developers and companies. Whether you’re a fresh graduate or a seasoned programmer, excelling in Ruby interview can open doors to top-tier tech companies. Preparing for these interviews sharpens your coding skills and enhances your ability to solve complex problems efficiently.
In this blog, we will cover:
20 essential Ruby interview questions categorized into:
Basic level
Intermediate level
Advanced level
Tips to excel in Ruby interviews, including key strategies and best practices.
Resources for further preparation, with links to courses and practice materials.
These questions test your foundational knowledge of Ruby, including its basic syntax and core concepts.
Ruby has four main types of variables:
Local variables: Declared within a function or block, accessible only within that specific scope, and typically named using lowercase letters or underscores.
Instance variables: Prefixed with @
, specific to instances of a class.
Class variables: Prefixed with @@
, shared across all instances of a class.
Global variables: Prefixed with $
, accessible anywhere in the program.
$global_variable = "I'm a global variable"class Example@@class_variable = "I'm a class variable"def initialize@instance_variable = "I'm an instance variable"enddef show_local_variablelocal_variable = "I'm a local variable"puts local_variableendend
In the code above, four types of variables are demonstrated. A global variable $global_variable
is accessible throughout the entire program. A class variable @@class_variable
is shared across all instances of the Example
class. Inside the initialize
method, an instance variable @instance_variable
is unique to each object created from the class. Within the show_local_variable
method, a local variable local_variable
is defined and used only within the method’s scope. Each variable type has specific accessibility based on where and how it’s defined.
Ruby provides three main access control levels:
Public: Methods accessible from anywhere.
Private: Methods can only be accessed within the current object.
Protected: Methods accessible within instances of the same class or subclasses.
Symbols and strings are both used to represent text in Ruby, but there are key differences:
Symbols are immutable, meaning they cannot be changed once created. They are typically used as identifiers (e.g., keys in hashes).
Strings are mutable, allowing changes to the content, and are used when the actual text needs to be modified.
# Using symbols as hash keyssymbol_hash = { key: "value" }puts "Symbol key access: #{symbol_hash[:key]}" # Output: "Symbol key access: value"# Using strings as hash keysstring_hash = { "key" => "value" }puts "String key access: #{string_hash["key"]}" # Output: "String key access: value"
The code above demonstrates two types of keys used in hashes. The first hash uses a symbol :key
as the key, which is more memory-efficient and commonly used to represent fixed identifiers. The second hash uses a string "key"
as the key, which is more flexible but less efficient for performance.
require
: Loads external libraries or Ruby files from library directories (e.g., Ruby’s standard library or installed gems).
require_relative
: Loads Ruby files relative to the directory of the current file. This is particularly useful for loading files within the same project structure, using the current file’s path as a reference.
# Loads the JSON library from Ruby's standard libraryrequire 'json'# Loads a file relative to the current file's pathrequire_relative 'my_file'
In the above code, the require 'json'
method loads the JSON library from Ruby’s standard library, enabling JSON parsing and generation features. On the other hand, the require_relative 'my_file'
method loads a file relative to the current file’s path, making it useful for including files within the same project or directory.
In Ruby, nil
and false
values are considered “falsy”, but represent different concepts.
nil
: Represents the absence of a value and is an object of NilClass
.
false
: A boolean value representing a negative result, an object of FalseClass
A block in Ruby is a chunk of code passed to a method to perform iteration or other operations. Blocks are not objects but can be converted into Proc
objects for reuse. Unlike methods, blocks do not have names.
# Defining a methoddef greet(name)puts "Hello, #{name}!"end# Calling the methodgreet("Alice") # Output: Hello, Alice!#Method to process numbers with a block during iterationdef process_numbers(numbers)# Define the parameter to receive values passed to the blocknumbers.each do |number|#The block multiplies each number by 2 and prints itputs number * 2endend# Calling the method with a blockprocess_numbers([1, 2, 3]) # Output: 2 4 6# Defining a method that accepts a blockdef greet_with_block(name)# The 'yield' keyword executes the block if provided;#'block_given?' checks for block presenceyield(name) if block_given?end# Passing a block to the methodgreet_with_block("Bob") { |name| puts "Hello, #{name}!" } # Output: Hello, Bob!
This code defines a greet
method that prints a greeting with a passed name. The greet("Alice")
call outputs, “Hello, Alice!” The process_numbers
method takes an array, and using a block, it doubles each number and prints the result, as seen in the output “2 4 6.”
The greet_with_block
method uses yield
to execute a block if provided, with block_given?
checking for the block’s presence. The call greet_with_block("Bob") { |name| puts "Hello, #{name}!" }
outputs, “Hello, Bob!”
The self
keyword refers to the current object or context in which the code is being executed. It can refer to objects depending on where it is used—within a class, method, or block. In short, the value of self
changes depending on where it’s used.
In instance methods, self
refers to the instance of the class.
In class methods, self
refers to the class itself.
In blocks, self
refers to the outer context defined by the block.
# Example 1: Using self within a classclass Person# The initialize method sets up an instance with a given namedef initialize(name)@name = name # @name is an instance variable that stores the nameend# A method to display the name and the current objectdef show_nameputs "The name is #{@name}." # Accesses the instance variable @nameputs "self refers to the current object: #{self}" # self points to the instance calling this methodendend# Creating an instance of the Person classperson = Person.new("Alice")# Calling the show_name method, which prints the name and the current objectperson.show_name# Output:# The name is Alice.# self refers to the current object: #<Person:0x00007fb058d60730># Example 2: Using self inside a class methodclass Car# A class method defined using self. It belongs to the class, not any instancedef self.car_typeputs "self refers to the class: #{self}" # self here points to the Car classendend# Calling the class method car_type directly on the Car classCar.car_type# Output:# self refers to the class: Car# Example 3: Using self inside an instance method and inside a blockclass Calculator# An instance method to add two numbers and optionally execute a blockdef add(a, b)puts "Inside instance method: #{self}" # self refers to the instance of Calculator calling this methodresult = a + b # Adds the two numbersyield(result) if block_given? # Executes the block passed to this method, passing the result to itendend# Creating an instance of Calculatorcalculator = Calculator.new# Calling the add method with two numbers and a blockcalculator.add(5, 3) do |result|# Inside the block, result holds the sum of the numbersputs "The result inside the block is #{result}."# self inside the block refers to the context in which the block is defined, here it's the main objectputs "Inside block: #{self}"end# Output:# Inside instance method: #<Calculator:0x00007f81e48af318># The result inside the block is 8.# Inside block: main
This code demonstrates the use of self
in different contexts in Ruby:
In Example 1, self
inside an instance method refers to the current object. The Person
class’s show_name
method accesses the instance variable @name
and prints the current object using self
.
In Example 2, self
inside a class method points to the class itself (Car
). The class method car_type
outputs the class name.
In Example 3, self
inside the add
method refers to the calculator
object because the method is being called on it. Inside the block, however, self
refers to the context where the block is defined, which is the main
object in this case. This is because blocks inherit the context of their definition, not the calling object.
These questions test a deeper understanding of Ruby’s advanced features, syntax, and functionality.
In Ruby, both classes and modules are used to group methods and variables together, but they serve different purposes and have different characteristics:
Classes are blueprints for creating objects (instances). They allow for inheritance, where one class can inherit methods and properties from another.
Modules are collections of methods and constants that cannot be instantiated. They are mainly used to share functionality across different classes through mixins, but unlike classes, they cannot be inherited.
# Defining a classclass Cardef initialize(make, model)@make = make@model = modelenddef info"#{@make} #{@model}"endend# Creating an instance of Carmy_car = Car.new("Toyota", "Corolla")puts my_car.info # Output: Toyota Corolla# Defining a modulemodule Drivabledef drive"Driving the car..."endend# Adding the module to a class using `include`class ElectricCar < Carinclude Drivableend# Creating an instance of ElectricCarmy_electric_car = ElectricCar.new("Tesla", "Model S")puts my_electric_car.info # Output: Tesla Model Sputs my_electric_car.drive # Output: Driving the car...# Module cannot be instantiated# drivable_car = Drivable.new # This will result in an error: `cannot instantiate module`
This code demonstrates classes and modules in Ruby. The Car
class is initialized with the make
and model
arguments, and the info
method returns a string containing them. An instance of Car
is created, and its info
method is called.
The Drivable
module defines a drive
method included in the ElectricCar
class, allowing its instances to use both info
and drive
.
Modules cannot be instantiated, so attempting to create an instance of Drivable
would cause an error.
The case
statement in Ruby checks multiple conditions, similar to a switch case in other languages. It simplifies complex if-else chains.
case fruitwhen 'apple'puts "An apple a day"when 'banana'puts "Bananas are rich in potassium"elseputs "Unknown fruit"end
This code demonstrates a case
statement in Ruby. It checks the value of fruit
:
If fruit
is 'apple'
, it prints “An apple a day.”
If fruit
is 'banana'
, it prints, “Bananas are rich in potassium.”
For any other value, it prints “Unknown fruit.”
In Ruby, the super
keyword is used within a child class method to call the corresponding overridden method in the parent class. This allows the child class to reuse or extend the behavior defined in the parent class. The super
keyword can be used with or without arguments, and its behavior varies accordingly:
Without arguments: It passes the same arguments from the child class method to the parent class method.
With arguments: It passes only the arguments provided explicitly to super
to the parent class method, overriding the default behavior of forwarding all arguments.
class Animaldef speak(num)puts "Animal makes #{num} sounds"endendclass Dog < Animaldef speak(num)super # Calls the parent class speak method with the default argument numputs "Woof! " * numendendclass Cat < Animaldef speak(num)super(2 * num) # Calls the parent class speak method with argument 2 * numputs "Meow! " * 2 * numendenddog = Dog.newdog.speak(2) # Output: Animal makes 2 sounds \n Woof! Woof!cat = Cat.newcat.speak(2) # Output: Animal makes 4 sounds \n Meow! Meow! Meow! Meow!
This code demonstrates method overriding and the use of super
in Ruby.
The Animal
class defines a speak
method that takes an argument num
and prints a message about the number of sounds the animal makes.
The Dog
class inherits from Animal
and overrides the speak
method. It uses super
without arguments, which calls the parent class’s speak
method with the same num
argument passed to the child class’s method.
The Cat
class also inherits from Animal
and overrides the speak
method. It calls the parent class’s speak
method with a modified argument (2 * num
) by explicitly using super(2 * num)
.
==
is used to compare the values of two objects.
eql?
is stricter and compares the value and the type of two objects. Ruby also uses eql?
under the hood to determine whether two hash keys are equal.
puts 5 == 5.0 # Output: true (Values are the same after type conversion)puts 5.eql?(5.0) # Output: false (Different types)puts 5 == "5" # Output: false (There’s' no type conversion, so values are not considered the same)hash = { 5 => "Five", 5.0 => "Five point zero" }puts hash[5] # Output: fiveputs hash[5.0] # Output: five point zero
The code above explains the following:
When the values are the same after type conversion, ==
returns true. Otherwise false.
The method eql?
is used internally for hash keys, so different values are retrieved when keys have a different type (integer vs. float).
Ruby provides several built-in iterators that enable traversal and manipulation of collections like arrays and hashes.
each
: The each
iterator loops through each element in a collection, such as an array or hash, and executes a code block for each element.
map
: The map
iterator creates a new array by transforming a code block to each element in the original collection.
select
: The select
iterator filters elements in a collection based on a boolean condition specified in the block, returning a new array with only the elements that meet the condition.
reduce
: The reduce
iterator (also known as inject
) accumulates a result by combining collection elements using the logic defined in the block, returning a single value after processing all elements.
each_with_index
: The each_with_index
iterator is similar to each, but it also provides the index of each element, allowing you to access both the value and its position in the collection.
Both Proc
and lambda
are objects representing blocks of code, but they behave differently, especially regarding argument handling and return behavior.
Lambda: A lambda
is strict about the number of arguments passed. If the wrong number is provided, it raises an ArgumentError
. After execution, it returns control to the caller, similar to a method.
Proc: A Proc
is more lenient with arguments. If fewer arguments are passed, it assigns nil
to the missing ones. However, if an explicit return
is called inside a Proc
, it returns control from the method where the Proc
was called (not just from the Proc
itself), which can exit the surrounding method prematurely. Without an explicit return
, a Proc
behaves similarly to a Lambda, returning control to the caller after execution.
# Lambda Example:my_lambda = lambda { |x| puts "The value is #{x}" }my_lambda.call(10) # Works fine# my_lambda.call # ArgumentError: wrong number of arguments# Proc Example:my_proc = Proc.new { |x| puts "The value is #{x}" }my_proc.call(10) # Works finemy_proc.call # Works fine, prints "The value is "
This code demonstrates the difference between a lambda
and a Proc
in Ruby.
If you call my_lambda.call
without an argument; it raises an ArgumentError
.
On the other hand, calling my_proc.call
without any arguments doesn’t raise an error and simply prints “The value is” because the missing argument is set to nil
.
String interpolation in Ruby allows you to embed expressions inside string literals using the #{}
syntax. The expressions inside the curly braces are evaluated, and the result is inserted into the string. This makes it easy to directly incorporate variables, method results, or calculations into strings.
Key points:
You can use variables, method calls, or even calculations inside the curly braces.
String interpolation is more concise and readable compared to string concatenation, which requires the use of +.
x = 5y = 10# Interpolating an expression into a stringmessage = "The sum of #{x} and #{y} is #{x + y}."puts message
This code demonstrates string interpolation in Ruby. It creates two variables, name
and age
, and then uses string interpolation to embed their values into the greeting
string. The result is a dynamically generated string that includes the variables’ values and is printed to the console using puts
.
These questions are for those who deeply understand Ruby’s inner workings and advanced features.
Ruby uses automatic garbage collection to manage memory. It employs a mark-and-sweep algorithm, which works in two phases:
Mark phase: The garbage collector identifies and marks objects still in use by traversing references.
Sweep phase: It removes no longer referenced objects, freeing up memory.
Ruby also uses incremental and generational garbage collection to optimize performance. Incremental garbage collection reduces the impact of garbage collection by processing in small chunks, minimizing long pauses during program execution. Generational garbage collection focuses on younger objects, which are more likely to be unused, improving efficiency.
Ruby provides the method_missing
method, which allows objects to intercept calls to undefined methods. When a method that doesn’t exist is called on an object, method_missing
is triggered, enabling dynamic method handling. This is useful in domain-specific languages (DSLs) or proxy objects, where undefined method calls are handled dynamically by overriding method_missing
.
class DynamicMethodHandlerdef method_missing(method, *args)puts "You tried to call #{method} with arguments #{args.join(', ')}"endendhandler = DynamicMethodHandler.newhandler.some_method(1, 2, 3) # This will call method_missing
This code demonstrates Ruby’s method_missing
feature. When a method that doesn’t exist is called on an object, the method_missing
method is triggered. In this example, calling some_method
on the handler
object invokes method_missing
, printing a message with the method name and arguments passed.
Singleton methods are defined on a single object rather than all class instances. These methods are specific to the object they are defined on and do not affect other objects of the same class.
Singleton methods are stored in the object’s hidden, unique singleton class (or eigenclass).
This mechanism allows dynamic behavior addition to individual objects.
Singleton methods are often used for creating class-level methods or for
str = "hello"def str.greet"Hi from #{self}"endputs str.greet # Outputs: Hi from helloanother_str = "world"# another_str.greet will raise a NoMethodError because greet is only defined for str.
This code demonstrates defining a method on a specific object. The greet
method is added to the str
object, and calling str.greet
outputs a greeting with the value of str
. However, since greet
is not defined for another_str
, calling another_str.greet
will raise a NoMethodError
.
Enumerator objects in Ruby allow you to create custom iteration behavior without directly implementing an iterator method. They encapsulate iteration logic, enabling lazy evaluations and allowing collections to be iterated in a reusable manner.
Key points:
Enumerators can be created with methods like each
, map
when they are used without a block, or manually using Enumerator.new
.
They allow chaining for lazy evaluation, which is useful for handling large collections.
Commonly used methods include next
, rewind
, and with_index
.
# Creating an Enumeratorenum = [1, 2, 3].eachputs enum.next # Output: 1puts enum.next # Output: 2puts enum.next # Output: 3# Using Enumerator for custom iterationfib = Enumerator.new do |y| # 'y' is the Enumerator object used to yield valuesa, b = 0, 1loop doy << a # 'y << a' sends the current value of 'a' to the Enumeratora, b = b, a + bendendputs fib.take(5) # Output: [0, 1, 1, 2, 3]
In the context of the custom Enumerator, y
represents a Yielder
object passed to the block. This object allows you to send values to the enumerator using the << operator, similar to how yield
is used to send values from a block. The values passed to the enumerator can then be accessed sequentially by calling its methods. For instance, the take(5)
method retrieves the first 5 values from the enumerator.
In Ruby, both include
and extend
are used to mix modules into classes, but they differ in scope and functionality:
include
: Adds the module’s methods as instance methods to the including class.
extend
: Adds the module’s methods as class methods to the extending class or object.
module Greetingsdef say_hello"Hello!"endendclass Personinclude Greetings # Adds as instance methodsextend Greetings # Adds as class methodsendputs Person.say_hello #Invoking the class method: Hello!puts Person.new.say_hello #Invoking the instance method: Hello!
This code demonstrates the use of modules in Ruby. The Greetings
module defines a method say_hello
. In the Person
class, include Greetings
adds the say_hello
method as an instance method, and extend Greetings
adds it as a class method. As a result, both the Person
class and its instances can call say_hello
.
Ruby applications, like any web application, may face common security attacks. Fortunately, these can be mitigated using best practices and built-in Ruby on Rails features:
Cross-site scripting (XSS):
Injecting malicious scripts into web pages.
Mitigation: Ensure user input is properly escaped in views to prevent malicious characters from being rendered as executable code. Avoid using html_safe
unless you are certain the content is safe. Use sanitize
to clean user input, removing potentially harmful tags or scripts.
Cross-site request forgery (CSRF):
Tricking users into performing actions without their consent.
Mitigation: Rails provides built-in CSRF protection by including a unique token in forms (via protect_from_forgery
), tied to the user’s session. This ensures that form submissions are intentional and secure.
SQL injection:
Injecting malicious SQL code into database queries.
Mitigation: Use ActiveRecord’s query parameterization methods (where
, find_by
) instead of string interpolation in queries to separate the user input from the actual SQL query.
Session hijacking and fixation:
Stealing or reusing session IDs to impersonate users.
Mitigation: To protect sessions from unauthorized access, use secure cookies, rotate session IDs upon login, and enable HTTPS.
By following these principles, Ruby developers can secure applications against the most common threats.
Mastering Ruby interview questions is key to excelling in technical interviews and showcasing expertise. Focus on Ruby fundamentals, explore advanced features, and practice regularly to build confidence and impress potential employers.
Continue honing your skills with coding challenges, real-world projects, and deeper dives into Ruby concepts.
Explore the following resources by Educative for an in-depth understanding and practical implementation of Ruby and its framework:
Course on Ruby:
Guide to Ruby interviews:
Standalone projects with Ruby on Rails:
Free Resources