Ruby array and hash interview questions, with answers
Arrays and hashes carry most of the data in a Ruby program, from a list of records to the params of a Rails request. Their methods are rich enough that interviewers use them to see whether a candidate writes idiomatic Ruby or translates loops from another language line by line.
The answers below cover reading arrays, adding and removing elements, hashes, iteration, merging, cleaning up arrays and splat assignment, with code run on Ruby 3.4. Then take the free Ruby diagnostic — ten questions across every Ruby topic in the bank — to see which of these you can explain but not yet predict.
The questions, with answers
1.How do you read elements of a Ruby array safely?
In short: Use first and last for the ends, [i] for a position, and fetch when a missing index should raise an error or give a default.
Arrays are ordered, zero-indexed and can hold values of any type. a[i] returns the element at i, or nil when the index is out of range, which can hide bugs. first and last read the ends and take an optional count. fetch(i) raises IndexError for a missing index, and fetch(i, default) returns the default instead, so the intent is explicit. dig reads nested arrays and hashes in one call, returning nil if any step is missing. Elements are added with push or its alias <<, which both return the array.
a = [3, 1, 2] a.push(4) a << 5 p a.first, a.last p a.fetch(9, 0) # 3 # 5 # 0
2.What do push, pop, shift and unshift do in Ruby?
In short: push adds to the end and pop removes from the end; unshift adds to the front and shift removes from the front.
These four methods let one array act as a stack or a queue. push and pop work at the end, so together they give last-in, first-out stack behaviour. unshift and shift work at the front; pushing at the end and shifting from the front gives a first-in, first-out queue. pop and shift return the removed element, or nil for an empty array, and all four change the array in place. insert and delete_at work at any position, and delete removes every element equal to a value. For heavy queue use, Thread::Queue adds thread safety.
q = [2, 3] q.unshift(1) q.push(4) p q.shift, q.pop, q # 1 # 4 # [2, 3]
3.How do you create and read a hash in Ruby?
In short: Write { key: value } for symbol keys or { "k" => v } for others; read with [] (nil if missing) or fetch (raises if missing).
A hash maps keys to values and remembers insertion order. The literal { name: "Ana" } uses symbol keys, the most common kind, while the arrow form, { "name" => "Ana" }, works for any key. h[key] returns the value or nil when the key is absent, fetch(key) raises KeyError, which catches typos early, and fetch(key, default) supplies a fallback. key? checks presence, dig reads nested hashes, and to_a turns a hash into [key, value] pairs. Any object with consistent hash and eql? methods can be a key.
user = { name: "Mia", age: 21 } p user[:name] p user[:city] p user.fetch(:age) # "Mia" # nil # 21
4.How do you iterate over a hash in Ruby?
In short: each yields every key and value pair in insertion order; map, select and sum work on pairs too.
hash.each { |key, value| ... } visits every entry in the order the keys were inserted, and each_key and each_value visit just one side. The Enumerable methods also work, treating each entry as a [key, value] pair. On a hash, select and reject return a hash, while map returns an array, so map followed by to_h builds a new hash. transform_values and transform_keys build a new hash with every value or key changed, and filter_map combines filtering and mapping.
prices = { tea: 10, pie: 40 } prices.each do |item, cost| puts "#{item}: #{cost}" end # tea: 10 # pie: 40
5.How do you merge two hashes in Ruby?
In short: a.merge(b) returns a new hash where b's value wins for a shared key; a block decides the value for conflicts.
merge combines two hashes into a new one without changing either: keys from both appear, and when a key is in both, the argument's value replaces the receiver's. Passing a block changes that rule, since the block receives the key, the old value and the new value and returns the value to keep, which is how counts or lists are combined. merge! and its alias update change the receiver in place. For default options, defaults.merge(options) lets the caller's options win.
a = { x: 1, y: 2 } b = { y: 5 } p a.merge(b) c = a.merge(b) do |_k, o, n| o + n end p c # {x: 1, y: 5} # {x: 1, y: 7}
6.How do you remove nils and duplicates from a Ruby array?
In short: compact drops nil elements, uniq drops duplicates, flatten removes nesting and sort orders the result, each returning a new array.
Cleaning up data is usually a chain of non-mutating methods. flatten turns nested arrays into one level, or as many levels as its argument says. compact removes nil elements, uniq removes duplicates, keeping the first occurrence, and accepts a block to decide what counts as a duplicate, and sort orders the elements, with sort_by for a derived key. Each returns a new array, and each has a bang version, such as compact!, that changes the array in place and returns nil when nothing changed.
a = [3, nil, 1, 3, [2]] p a.flatten.compact.uniq.sort # [1, 2, 3]
7.What does the splat operator * do with Ruby arrays?
In short: In an assignment, *rest collects the remaining elements; in a call, *array spreads an array into separate arguments.
The splat collects or spreads elements depending on where it appears. On the left of an assignment, first, *rest = [1, 2, 3] puts 1 in first and [2, 3] in rest, and the splat can sit in the middle as well. In a method definition, def sum(*nums) collects any number of arguments into an array, and in a call, sum(*list) spreads an array into separate arguments. Inside an array literal, [*a, *b] concatenates. The double splat, **, does the same for keyword arguments and hashes.
first, *rest = [1, 2, 3] p first, rest # 1 # [2, 3]
How the diagnostic asks it
One question from the Ruby bank, exactly as a sitting would show it. The bank has 4 on arrays & hashes and 30 across Ruby.
What does this Ruby code print?
h = { "id" => 1, id: 2 } p h["id"] p h[:id] p h.size
- 12, then 2, then 1
- 21, then 1, then 1
- 31, then nil, then 1
- 41, then 2, then 2correct
In a hash literal, id: 2 is shorthand for :id => 2, a symbol key, while "id" => 1 uses a String key. A symbol and a string are never equal, so they are two separate keys: h["id"] is 1, h[:id] is 2, and the hash has 2 entries. The option with a single entry assumes the later key overwrote the earlier one. nil for :id assumes the shorthand makes a string key. Mixing the two is a common bug with data parsed from JSON, whose keys are strings, unless the parser is asked for symbols; Rails' HashWithIndifferentAccess exists to paper over it.
Measure it
Reading answers tells you what’s true. A diagnostic tells you what you get wrong.
10 Ruby questions across its topics, easy to hard, about fifteen minutes. You get a readiness figure with the arithmetic shown, the topics you missed named, and a practice set sized for today. Free: 1 diagnostic a month and 15 problems a day. No card.