rubyguides

Array#push

Overview

Appends one or more elements to the end of an array. The collection is modified in place, and the method returns the same array object.

Basic Usage

fruits = ["apple", "banana"]
fruits.push("cherry")
# => ["apple", "banana", "cherry"]

fruits
# => ["apple", "banana", "cherry"]

push mutates the original: no new object is created, and the return value is the same object. You can verify this by comparing object IDs before and after the call. This mutation behaviour is shared across all the bang-less mutating array methods in Ruby, not just push. The design means you get efficient in-place updates but need to be mindful when the data is shared across multiple references that might not expect it to grow.

Appending multiple elements

Unlike the << operator, push accepts multiple arguments:

queue = [1, 2]
queue.push(3, 4, 5)
# => [1, 2, 3, 4, 5]

Passing multiple arguments to push appends each one in left-to-right order, preserving the sequence you provide. Under the hood, Ruby grows the internal buffer as needed and copies each argument into the next available slot. The total cost is proportional to the number of arguments plus any reallocation that may be triggered, but the amortised per-element cost remains constant because reallocations double the capacity and happen less frequently as the array grows.

letters = []
letters.push("a")
letters.push("b", "c")
# => ["a", "b", "c"]

push vs <<

The << operator appends exactly one element at a time and is the shorthand most Ruby developers reach for when adding a single item. It reads naturally as “shovel this on” and works well in chains because each call returns the modified array. The key difference from push is that << takes exactly one argument, not zero and not many.

nums = [1, 2]
nums << 3
# => [1, 2, 3]

Chaining << operators works because each call returns the modified result, making the next << available on the same line. This chaining style is common in Ruby scripts and one-liners where conciseness matters, but it can become hard to read when the chain grows beyond two or three operators. For longer sequences or when the elements come from a splatted array, push with multiple arguments or concat is usually the clearer option.

push is cleaner when appending multiple unrelated elements or when the number of elements is dynamic:

items = []
new_items = ["a", "b", "c"]
items.push(*new_items)  # splat expands the array
# => ["a", "b", "c"]

The splat operator unpacks the source array so each element becomes a separate argument to push. Without it, calling items.push(new_items) would append the whole array as a single nested element. The splat turns “add this collection” into “add each member of this collection individually,” which is the same distinction that separates push from concat.

push vs concat

concat takes a single array argument and appends all of its elements:

a = [1, 2]
a.concat([3, 4])
# => [1, 2, 3, 4]

With a splat, push can mimic concat, but the semantics differ when the argument is a single array that you want to keep whole. concat always unpacks its argument, while push keeps the argument shape unless you explicitly splat. This is the fundamental design choice between the two: one is about adding individual values, the other is about merging collections.

a = [1, 2]
a.push(*[3, 4])
# => [1, 2, 3, 4]

append as an alias

append is an alias for push and compiles to the same C implementation, so there is no performance difference between the two. The choice is stylistic: push is more common and familiar from other languages, while append reads more like natural English and can feel clearer when the intent is purely about adding to a collection.

a = [1, 2]
a.append(3)
# => [1, 2, 3]

Both compile to the same C implementation, so there is no performance difference. push is more common and appears in nearly every Ruby codebase; append may read better when the intent is clearly to add to a collection and the word “push” feels too low-level for the domain.

a.push(4)
# => [1, 2, 3, 4]

Practical Examples

The examples below demonstrate push in real-world patterns: growing a list from user input, implementing a FIFO queue, conditional accumulation, and one-level flattening during construction. Each pattern shows how push integrates with other Ruby idioms like each, shift, and the splat operator to solve common data-collection problems without introducing specialised collection classes.

Building a list

selected = []
loop do
  item = gets.chomp
  break if item.empty?
  selected.push(item)
end

Reading from standard input and pushing each line into an array is a common pattern in command-line tools and REPL-style scripts. The loop breaks on an empty line, at which point the list holds every non-empty input the user provided. Because push preserves insertion order, the result matches the sequence in which the lines were entered.

queue = []
queue.push("task1")
queue.push("task2")

current = queue.shift
# => "task1"
queue
# => ["task2"]

push adds to the back and shift removes from the front, together forming a classic FIFO queue. This pairing is one of the most common patterns in Ruby scripting because it requires no special data structure: a plain array with two methods gives you everything you need for job queues, breadth-first traversal, and event processing. The collection grows and shrinks naturally as items are enqueued and dequeued.

results = []
[1, 2, 3, 4, 5].each do |n|
  results.push(n * 2) if n.even?
end
results
# => [4, 8]

The conditional push inside an each loop collects only the elements that pass a test. Using each with a bare array is more memory-efficient than map followed by compact or select because it never allocates intermediate arrays. The trade-off is verbosity: the explicit loop spells out what is happening, which can be either a clarity win or unnecessary ceremony depending on how complex the filtering logic is.

Flattening while building

layers = [[1, 2], [3, 4], [5, 6]]
flat = []
layers.each { |layer| flat.push(*layer) }
flat
# => [1, 2, 3, 4, 5, 6]

Without the splat, layers.each { |layer| flat.push(layer) } would produce a nested array where each inner array becomes a single element of flat. The splat flattens one level, turning [[1,2], [3,4]] into [1,2,3,4] in one pass without needing to call flatten afterward.

# Without splat: nesting is preserved
nested = []
[[1, 2], [3, 4]].each { |layer| nested.push(layer) }
nested
# => [[1, 2], [3, 4]]

Return Value

push always returns the same object that was modified, never a copy or a new array. You can verify this by comparing the object_id of the original and the return value; they will always be identical because both references point to the same underlying array in memory.

original = ["a", "b"]
returned = original.push("c")
returned.object_id == original.object_id
# => true

Because push returns self, you can chain calls on the same array without storing an intermediate reference. The chain works because each push mutates and returns the array, making the next method call available immediately. This is especially convenient in builder-style code where you construct a collection through a series of method calls rather than through repeated assignment statements, letting the chain read from left to right as a sequence of additions.

[].push("a").push("b")
# => ["a", "b"]

Performance Notes

Appending to the end of a Ruby array is amortized O(1). Ruby maintains an internal growable buffer of extra capacity beyond the visible count, so most push calls simply write into an already-allocated slot without triggering a resize. When the buffer runs out, Ruby doubles the array’s capacity and copies all existing elements into the new, larger buffer. This doubling strategy means reallocations become exponentially less frequent as the array grows, keeping the average cost per push constant even for arrays that grow to millions of elements. Because of this design, building by repeatedly calling push inside a loop is efficient and idiomatic: you get clean code and predictable performance without needing to pre-allocate or manage capacity by hand.

See Also