Ruby Arrays: A Visual, Hands-On Guide
Learn Ruby arrays visually: creating and reading them, adding and removing at both ends, stacks and queues, each, map, select and friends, sorting, combining, nested arrays, the gotchas, a real Array vs. Set benchmark and exercises with answers.

On this page
A playlist, a shopping cart, the high-score table, the players in a match, the lines of a file: programs are full of lists. In Ruby, a list is an Array, and Ruby's arrays are among the friendliest in any language. They grow and shrink on their own, hold anything you put in them, and come with a huge set of methods for searching, sorting and transforming.
In this guide you'll see how an array is laid out, add and remove elements at both ends, build a stack and a queue, chain iterators like map and select into tidy pipelines, sort and combine arrays, dodge two classic gotchas, and measure when an array is the wrong tool. Exercises with answers wrap it up.
All the outputs below come from real runs with Ruby 3.3. The examples use p, which shows arrays with their brackets and quotes.
One name, many boxes
Instead of five separate variables for five animals, an array keeps them all under one name, in order:
![An array is a numbered shelf: one name, many boxes. The array animals holds "dog", "cat", "bear", "horse" and "eagle" at indexes 0 to 4, which are also -5 to -1 counting from the end. animals[1, 3] gives ["cat", "bear", "horse"]. animals.first is "dog", animals.last is "eagle", animals.size is 5, and animals[10] is nil. Use fetch(10) if you'd rather get an error](https://nbktechworld.us-southeast-1.linodeobjects.com/static/images/74df1d43-db3c-4335-bc89-ad23006b401b.png?v=1792858305197)
Creating arrays
1animals = ["dog", "cat", "bear", "horse", "eagle"]
2mixed = [12, "dog", 3.65, nil, true, [1, 2]]
3words = %w[apple banana cherry]
4zeros = Array.new(3, 0)
5squares = Array.new(5) { |index| index * index }
6digits = (1..5).to_a
7
8p animals
9p mixed
10p words
11p zeros
12p squares
13p digits
14p [].empty?1["dog", "cat", "bear", "horse", "eagle"]
2[12, "dog", 3.65, nil, true, [1, 2]]
3["apple", "banana", "cherry"]
4[0, 0, 0]
5[0, 1, 4, 9, 16]
6[1, 2, 3, 4, 5]
7true- Square brackets with commas are the usual way.
- A Ruby array can mix any kinds of values, even other arrays.
%w[...]is a shortcut for an array of single words.Array.new(5) { |index| … }builds an array by running the block once per position.
Reading elements
1animals = ["dog", "cat", "bear", "horse", "eagle"]
2p animals[0]
3p animals[4]
4p animals[-1]
5p animals.first
6p animals.last(2)
7p animals[1, 3]
8p animals[1..3]
9p animals.size
10p animals[10]
11begin
12 animals.fetch(10)
13rescue IndexError => error
14 puts "fetch refused: #{error.message}"
15end1"dog"
2"eagle"
3"eagle"
4"dog"
5["horse", "eagle"]
6["cat", "bear", "horse"]
7["cat", "bear", "horse"]
85
9nil
10fetch refused: index 10 outside of array bounds: -5...5Counting starts at 0, so the last element is at size - 1, or simply at -1. Asking for a position that isn't there gives nil, quietly. Use fetch when a missing element should be an error.
Adding and removing elements

1animals = ["dog", "cat", "bear"]
2
3animals.push("horse")
4animals << "eagle"
5p animals
6
7removed = animals.pop
8puts "pop removed #{removed.inspect}"
9p animals
10
11animals.unshift("dinosaur")
12p animals
13
14first_one = animals.shift
15puts "shift removed #{first_one.inspect}"
16p animals1["dog", "cat", "bear", "horse", "eagle"]
2pop removed "eagle"
3["dog", "cat", "bear", "horse"]
4["dinosaur", "dog", "cat", "bear", "horse"]
5shift removed "dinosaur"
6["dog", "cat", "bear", "horse"]In the middle
1animals = ["dog", "cat", "bear", "horse", "eagle"]
2animals[1] = "turtle"
3p animals
4
5animals.insert(2, "owl")
6p animals
7
8animals.delete("bear")
9p animals
10
11removed = animals.delete_at(0)
12puts "delete_at(0) removed #{removed.inspect}"
13p animals1["dog", "turtle", "bear", "horse", "eagle"]
2["dog", "turtle", "owl", "bear", "horse", "eagle"]
3["dog", "turtle", "owl", "horse", "eagle"]
4delete_at(0) removed "dog"
5["turtle", "owl", "horse", "eagle"]delete("bear") removes by value (every copy of it); delete_at(0) removes by position.
Stacks and queues
Those four end methods make an array a ready-made stack or queue, two of the most useful patterns in programming:

1# A stack: the last thing in is the first thing out (like undo)
2history = []
3history.push("type 'Hello'")
4history.push("make it bold")
5history.push("change the color")
6puts "Undo: #{history.pop}"
7puts "Undo: #{history.pop}"
8p history
9
10# A queue: the first thing in is the first thing out (like a line at a shop)
11print_queue = []
12print_queue.push("report.pdf")
13print_queue.push("photo.jpg")
14print_queue.push("ticket.png")
15puts "Printing: #{print_queue.shift}"
16puts "Printing: #{print_queue.shift}"
17p print_queue1Undo: change the color
2Undo: make it bold
3["type 'Hello'"]
4Printing: report.pdf
5Printing: photo.jpg
6["ticket.png"]Going through an array
1animals = ["dog", "turtle", "bear"]
2
3animals.each { |animal| puts animal }
4
5animals.each_with_index do |animal, index|
6 puts "#{index + 1}. #{animal}"
7end
8
9index = 0
10while index < animals.length
11 puts animals[index].upcase
12 index += 1
13end1dog
2turtle
3bear
41. dog
52. turtle
63. bear
7DOG
8TURTLE
9BEAReach hands every element, in order, to the block between the braces (or between do and end). each_with_index adds its position. The while loop works too, but in Ruby you'll almost always see each: there's no index to manage, and no way to go off the end.
The iterator toolbox: map, select and friends
Beyond each, arrays have iterators that build something new from the elements. These are the ones you'll use daily:

1prices = [12.5, 3.0, 45.0, 7.25, 19.99, 2.5]
2
3p prices.map { |price| (price * 1.1).round(2) }
4p prices.select { |price| price > 10 }
5p prices.reject { |price| price > 10 }
6p prices.find { |price| price > 10 }
7p prices.count { |price| price < 5 }
8p prices.sum
9p prices.min
10p prices.max
11p prices.sum / prices.size
12p prices.reduce { |total, price| total + price }1[13.75, 3.3, 49.5, 7.98, 21.99, 2.75]
2[12.5, 45.0, 19.99]
3[3.0, 7.25, 2.5]
412.5
52
690.24
72.5
845.0
915.04
1090.24| Method | Gives you | Think of it as |
|---|---|---|
map | a new array, same size | "turn each one into…" |
select | the elements that match | "keep the ones where…" |
reject | the elements that don't | "throw out the ones where…" |
find | the first match (or nil) | "find me one where…" |
count | how many match | "how many are…" |
sum, min, max | one number | "add up / smallest / biggest" |
reduce | one value built step by step | "combine them all into…" |
Chaining them into a pipeline
Because each of these returns a new array, you can chain them, one step per line:

1orders = [
2 { item: "coffee", price: 3.5, paid: true },
3 { item: "cake", price: 4.25, paid: false },
4 { item: "sandwich", price: 7.0, paid: true },
5 { item: "juice", price: 4.0, paid: true },
6]
7
8total_paid = orders
9 .select { |order| order[:paid] }
10 .map { |order| order[:price] }
11 .sum
12
13puts "Paid so far: #{total_paid}"Paid so far: 14.5Each order here is a hash. Arrays of hashes are how Ruby programs usually hold a list of records.
Sorting, counting and grouping
1scores = [72, 95, 88, 61, 95, 90]
2names = ["eve", "Bob", "alice", "Dave"]
3
4p scores.sort
5p scores.sort.reverse
6p scores.sort { |first, second| second <=> first }
7p scores.uniq
8p scores.max(3)
9p names.sort
10p names.sort_by(&:downcase)
11p names.max_by(&:length)
12p scores.tally
13p scores.group_by { |score| score >= 90 ? "A" : "not A" }1[61, 72, 88, 90, 95, 95]
2[95, 95, 90, 88, 72, 61]
3[95, 95, 90, 88, 72, 61]
4[72, 95, 88, 61, 90]
5[95, 95, 90]
6["Bob", "Dave", "alice", "eve"]
7["alice", "Bob", "Dave", "eve"]
8"alice"
9{72=>1, 95=>2, 88=>1, 61=>1, 90=>1}
10{"not A"=>[72, 88, 61], "A"=>[95, 95, 90]}- Plain
sortputs uppercase letters before lowercase ("Bob"before"alice").sort_by(&:downcase)sorts the way people expect. <=>is the "spaceship" operator, which compares two values. Swapping the two sides sorts in reverse.&:downcaseis shorthand for{ |name| name.downcase }.tallycounts each value;group_bysorts elements into buckets.
Combining arrays

1mine = ["apple", "banana", "cherry"]
2yours = ["banana", "cherry", "date"]
3
4p mine + yours
5p mine - yours
6p mine & yours
7p mine | yours
8p [[1, 2], [3, [4, 5]]].flatten
9p ["Ana", "Bruno", "Carla"].zip([90, 85, 77])
10p (1..10).each_slice(3).to_a
11p [1, 2, 3, 4].each_cons(2).to_a1["apple", "banana", "cherry", "banana", "cherry", "date"]
2["apple"]
3["banana", "cherry"]
4["apple", "banana", "cherry", "date"]
5[1, 2, 3, 4, 5]
6[["Ana", 90], ["Bruno", 85], ["Carla", 77]]
7[[1, 2, 3], [4, 5, 6], [7, 8, 9], [10]]
8[[1, 2], [2, 3], [3, 4]]zip pairs up two arrays like the teeth of a zipper; each_slice(3) cuts an array into groups of 3; each_cons(2) gives every pair of neighbors.
Arrays inside arrays
An array of arrays makes a grid: rows of columns.
1board = [
2 ["X", "O", "X"],
3 [" ", "X", "O"],
4 ["O", " ", "X"],
5]
6
7board.each { |row| puts row.join(" | ") }
8p board[1][2]
9p board.map { |row| row[0] }
10p board.transpose[0]
11diagonal = (0..2).map { |index| board[index][index] }
12puts "Diagonal: #{diagonal.join}"
13puts "X wins the diagonal!" if diagonal.all?("X")1X | O | X
2 | X | O
3O | | X
4"O"
5["X", " ", "O"]
6["X", " ", "O"]
7Diagonal: XXX
8X wins the diagonal!board[1][2] is row 1, column 2. transpose swaps rows and columns, so board.transpose[0] is the first column.
Two gotchas
Gotcha 1: Array.new(3, []) shares one inner array
![Array.new(3, []) shares one inner array. With Array.new(3, []), all three slots point to the same array, so grid[0] << "oops" shows up in every row. With Array.new(3) { [] }, the block runs once per slot, giving three separate arrays](https://nbktechworld.us-southeast-1.linodeobjects.com/static/images/574921e1-95f3-4b7d-9ac3-1683dfde7f8d.png?v=1792858305368)
1grid = Array.new(3, [])
2grid[0] << "oops"
3p grid
4
5safe_grid = Array.new(3) { [] }
6safe_grid[0] << "fine"
7p safe_grid[["oops"], ["oops"], ["oops"]]
[["fine"], [], []]Array.new(3, []) creates one empty array and puts it in all three slots. Changing it through one slot changes "all of them", because they're the same array. The block form runs once per slot and creates a fresh array each time. (Numbers, as in Array.new(3, 0), are safe because you can't change a number in place.)
Gotcha 2: methods with ! change the original
1numbers = [3, 1, 2]
2sorted = numbers.sort
3p numbers
4p sorted
5
6numbers.sort!
7p numbers
8
9doubled = numbers.map { |number| number * 2 }
10p doubled
11numbers.map! { |number| number * 10 }
12p numbers1[3, 1, 2]
2[1, 2, 3]
3[1, 2, 3]
4[2, 4, 6]
5[10, 20, 30]sort, map, select, reverse and uniq return a new array; their ! twins (sort!, map!, select!…) change the array in place. Methods like push, pop, << and delete change the array even without a !.
When an array is the wrong tool: searching
include? answers "is this in the list?":
1require "set"
2
3guest_list = ["Ana", "Bruno", "Carla"]
4p guest_list.include?("Bruno")
5p guest_list.include?("Zeca")
6p guest_list.index("Carla")
7
8guests = Set.new(guest_list)
9p guests.include?("Bruno")1true
2false
32
4trueBut to answer that, an array has to look at its elements one by one. A Set is built for exactly this question: it jumps straight to the answer. We measured 10,000 checks against lists of growing size:

With 100,000 names, the array needed 11.5 seconds; the Set, 3.2 milliseconds: about 3,600 times faster. Every time the list grew 10×, the array's time grew about 10× too, while the Set barely noticed.
A map of everyday array methods
![A map of everyday array methods. Create: [1, 2, 3], %w[a b c], Array.new(3) with a block, (1..5).to_a. Read: [0] and [-1], first and last, [1, 3] and [1..3], size and empty?. Add and remove: push and <<, pop, unshift and shift, insert and delete. Loop: each, each_with_index, each_slice, while. Transform: map, select and reject, find, reduce. Measure: sum, min and max, count, tally. Order: sort and sort_by, reverse, uniq, max_by. Combine: + and -, & and |, zip, flatten](https://nbktechworld.us-southeast-1.linodeobjects.com/static/images/118a6f3c-5f5f-4162-950e-cfab8fb1d0df.png?v=1792858305428)
Practice time
Try these before peeking at the answers.
Exercise 1: Predict the output (warm-up). What does this print?
1line = [1, 2, 3]
2line.push(4)
3line.shift
4line.unshift(0)
5line.pop
6p lineExercise 2: Shopping cart. Given a cart of items with a price and a quantity, print each line with its subtotal, then the total.
Exercise 3: Duplicates. A sign-up list has repeats: ["ana", "bruno", "ana", "carla", "bruno", "ana", "diego"]. Print the list without duplicates (keeping the original order), how many duplicates were removed, and which names signed up more than once.
Exercise 4: Teams. Split 10 players into teams of 3 (the last team gets whoever's left) and print each team with its number.
Exercise 5: Leaderboard. Given pairs of [name, points], print the top 3, best first, with their positions.
Exercise 6: Rotate a picture (challenge). A tiny picture is stored as rows of characters. Turn it 90° to the right. (Hint: think about what transpose does, and then what's still backwards.)
Quick quiz
- What's the index of the last element of a 5-element array? Give two ways to write it.
- Which pair of methods makes a stack, and which makes a queue?
- What's the difference between
mapandeach? - Why does
Array.new(3, [])surprise people? - When should you reach for a
Setinstead of an array?
Answers
Exercise 1: Step by step: [1, 2, 3, 4], then shift removes the 1, unshift(0) adds 0 at the front, and pop removes the 4:
1line = [1, 2, 3]
2line.push(4)
3line.shift
4line.unshift(0)
5line.pop
6p line[0, 2, 3]Exercise 2: each for the lines, and sum with a block for the total:
1cart = [
2 { item: "notebook", price: 4.5, quantity: 3 },
3 { item: "pen", price: 1.25, quantity: 4 },
4 { item: "backpack", price: 32.0, quantity: 1 },
5]
6
7cart.each do |line|
8 subtotal = line[:price] * line[:quantity]
9 puts "#{line[:item].ljust(10)} #{line[:quantity]} x #{format('%5.2f', line[:price])} = #{format('%6.2f', subtotal)}"
10end
11total = cart.sum { |line| line[:price] * line[:quantity] }
12puts "Total: #{format('%.2f', total)}"1notebook 3 x 4.50 = 13.50
2pen 4 x 1.25 = 5.00
3backpack 1 x 32.00 = 32.00
4Total: 50.50Exercise 3: uniq keeps the first copy of each name, in order; tally finds the repeaters:
1sign_ups = ["ana", "bruno", "ana", "carla", "bruno", "ana", "diego"]
2unique = sign_ups.uniq
3puts "Unique: #{unique.inspect}"
4puts "Removed #{sign_ups.size - unique.size} duplicates"
5p sign_ups.tally.select { |name, count| count > 1 }.keysUnique: ["ana", "bruno", "carla", "diego"]
Removed 3 duplicates
["ana", "bruno"]Exercise 4: each_slice(3) makes the groups, and with_index(1) numbers them from 1:
1players = %w[Ana Bruno Carla Diego Eva Fabio Gabi Hugo Iris Joao]
2players.each_slice(3).with_index(1) do |team, number|
3 puts "Team #{number}: #{team.join(', ')}"
4end1Team 1: Ana, Bruno, Carla
2Team 2: Diego, Eva, Fabio
3Team 3: Gabi, Hugo, Iris
4Team 4: JoaoExercise 5: Sort by negative points to get the highest first, take 3, and unpack each pair in the block with (name, points):
1scores = [["Ana", 1200], ["Bruno", 950], ["Carla", 1430], ["Diego", 800], ["Eva", 1310]]
2
3top_three = scores.sort_by { |name, points| -points }.first(3)
4top_three.each_with_index do |(name, points), index|
5 puts "#{index + 1}. #{name.ljust(6)} #{points}"
6end1. Carla 1430
2. Eva 1310
3. Ana 1200Exercise 6: transpose turns columns into rows; reversing each of those rows finishes the turn to the right:
1picture = [
2 ["#", ".", "."],
3 ["#", "#", "."],
4 ["#", "#", "#"],
5]
6
7rotated = picture.transpose.map(&:reverse)
8
9puts "Before:"
10picture.each { |row| puts row.join }
11puts "After turning right:"
12rotated.each { |row| puts row.join }1Before:
2#..
3##.
4###
5After turning right:
6###
7##.
8#..Quiz: (1) Index 4, which you can write as array[4], array[-1] or array.last. (2) A stack uses push + pop (last in, first out); a queue uses push + shift (first in, first out). (3) each just visits the elements and returns the original array; map builds and returns a new array from what the block returns. (4) It puts the same array in every slot, so changing one changes them all; use Array.new(3) { [] }. (5) When the main question is "is this in here?", especially for big lists: in our test, a Set answered about 3,600 times faster with 100,000 names.
Cheat sheet
| I want to… | Write |
|---|---|
| Make an array | [1, 2, 3], %w[a b c], Array.new(3) { … }, (1..5).to_a |
| Read an element | list[0], list[-1], list.first, list.last |
| Read a slice | list[1, 3], list[1..3] |
| Count elements | list.size, list.count { … }, list.empty? |
| Add to the end / front | list.push(x) or list << x / list.unshift(x) |
| Remove from the end / front | list.pop / list.shift |
| Insert / delete in the middle | list.insert(2, x) / list.delete(x), list.delete_at(2) |
| Visit every element | list.each { |item| … }, each_with_index |
| Transform / filter | map { }, select { }, reject { }, find { } |
| Add up / extremes | sum, min, max, max_by { } |
| Sort | sort, sort_by { }, reverse |
| Remove duplicates / count values | uniq / tally |
| Combine | +, -, &, |, zip, flatten |
| Cut into groups | each_slice(3) |
| Check membership fast | require "set", then Set.new(list).include?(x) |
Wrapping up
Arrays are Ruby's everyday list, and they can do a lot:
- Indexes start at 0, negative indexes count from the end, and a missing index gives
nil(or an error withfetch). push/popandunshift/shiftwork at the two ends, which makes stacks and queues trivial.eachvisits;map,select,reject,find,sumand friends compute, and chain into readable pipelines.sort,uniq,tally,group_by,zipand the set operators answer most questions about a list in one line.- Watch for shared defaults (
Array.new(3) { [] }, notArray.new(3, [])) and for methods that change the array in place. - For "is it in here?" on big lists, use a
Set: about 3,600 times faster than an array with 100,000 names in our test.
Want more Ruby? Read What Is the Ruby Programming Language?, then Ruby String Methods, Ruby Hashes and Basic File I/O in Ruby.
Looking for a reference text for Ruby? Get the Well-Grounded Rubyist.
Read next

Ruby Blocks and Iterators: Write Your Own
Learn how Ruby methods receive blocks, pass values with yield, handle a missing block, and return an Enumerator. Build and run your own iterator.

C++ std::vector: Size, Capacity, and Safe Growth
Learn how std::vector grows in C++: the difference between size and capacity, when reserve helps, why resize is different, and how growth can invalidate references.

How to Handle Errors in Go: A Hands-On Guide
Learn Go's everyday error pattern with runnable examples: return an error, check it, add context with %w, and recognize a missing file with errors.Is.
Comments
Be kind and specific. Comments with links from new accounts appear after a moderator reviews them.