Stacking and Splitting

Joining arrays along an existing axis or a new one - and why the two are different functions.

Overview

Two different questions

You have several arrays and want one. There are two distinct things that could mean, and NumPy gives them separate functions rather than guessing.

concatenate joins along an axis that already exists. Two (2, 3) arrays concatenated on axis 0 give (4, 3) — more rows, same number of dimensions.

stack creates a new axis. The same two arrays stacked give (2, 2, 3) — a pile of two frames, one dimension more than the inputs.

Asking which you want is the first step, and the answer is usually obvious once phrased that way: appending records is concatenation; collecting frames or samples into a batch is stacking.

Worth knowing

concatenate joins along an existing axis; stack creates a new one.
For concatenate, every axis except the joining one must match. For stack, the shapes must be identical.
vstack and hstack are conveniences. On 1-D input they differ sharply: vstack makes rows, hstack joins end to end.
column_stack turns 1-D arrays into columns, which is usually what people reach for hstack hoping to get.
split requires equal parts and raises otherwise; array_split allows uneven ones.
Never concatenate in a loop — each call copies everything so far. Collect into a list and stack once.

Stacking and Splitting

Joining along an existing axis or a new one, and why those are different functions.

concatenate joins along an existing axis

The arrays must already agree on every other axis. Nothing new is created.

example_01.pyNumPy
Output

stack creates a NEW axis

That is the whole difference. Same inputs, one more dimension out.

example_02.pyNumPy
Output

vstack, hstack and the 1-D surprise

Convenient names, and a special case for 1-D input that catches people.

example_03.pyNumPy
Output

Shapes must line up

The error names the axis that disagrees, which is usually enough to find the problem.

example_04.pyNumPy
Output

Splitting is the inverse

split needs equal parts; array_split does not.

example_05.pyNumPy
Output

Building in a loop is still wrong

Concatenating repeatedly is quadratic for the same reason appending is. Collect, then join once.

example_06.pyNumPy
Output

What must match

concatenate requires every axis except the joining one to agree. Joining on axis 0 needs matching column counts; joining on axis 1 needs matching row counts.

stack is stricter: all inputs must have exactly the same shape, since they are becoming parallel slices of a new axis.

The error messages name the mismatch, and reading them rather than guessing usually locates the problem immediately.

The convenience functions

vstack stacks vertically, hstack horizontally, dstack along the third axis. For 2-D input they are just concatenate with a fixed axis.

For 1-D input they diverge, and this is where they cause trouble.

np.vstack([a, b]) on two length-3 arrays gives (2, 3) — it promotes each to a row.

np.hstack([a, b]) gives (6,) — it joins them end to end, because axis 1 does not exist so it falls back to axis 0.

People reaching for hstack to make two columns get one long array instead. column_stack is the function that does what they meant: it turns 1-D arrays into columns of a 2-D result.

The general advice: use concatenate or stack with an explicit axis when the dimensionality might vary. The convenience names are fine when you know exactly what shapes you have.

Splitting

np.split(a, n, axis=0) divides into n equal parts and raises if the length does not divide exactly. That strictness is useful — an uneven split is usually a sign that something upstream is not the size you assumed.

np.array_split allows uneven parts, distributing the remainder across the first few. Use it when uneven is genuinely acceptable, such as chunking work for parallel processing.

split also accepts a list of positions rather than a count: np.split(a, [3, 7]) cuts before index 3 and before index 7, giving three pieces. That is often more natural than computing a count.

The pieces are views, not copies, which is worth knowing: splitting a large array is free, and writing into a piece writes into the original.

Do not build in a loop

The same rule as np.append, for the same reason.

Concatenating inside a loop allocates a new array and copies everything accumulated so far, on every iteration. That is quadratic, and the last editor measures the difference.

Collect into a Python list and call stack or concatenate once at the end. Lists append cheaply; arrays do not append at all.

If you know the final size in advance, better still: allocate with np.empty and assign into slices. That avoids even the single large copy.

Choosing

Appending records of the same width: concatenate(axis=0).

Adding columns to a table: concatenate(axis=1), or column_stack for 1-D inputs.

Collecting frames, samples or channels into a batch: stack, with an explicit axis.

Undoing any of those: split when the division is exact, array_split when it is not.

And in every case, if the joining is happening inside a loop, the answer is to move it outside.

r_ and c_

np.r_ and np.c_ are index-expression shortcuts, and they look strange because they use square brackets rather than parentheses.

np.r_[a, b] concatenates along the first axis. np.c_[a, b] stacks as columns, equivalent to column_stack.

They also accept slice syntax, so np.r_[0:5, 10, 20:23] builds an array from a mix of ranges and literals in one expression. That is genuinely convenient for constructing index arrays and test data.

They are compact rather than clear, and they show up more in older code and in interactive sessions than in libraries. Worth recognising; not worth preferring over the named functions in code others will read.

tile and repeat

Both make an array bigger by duplication, and they differ in a way that is easy to state and easy to forget.

np.tile(a, n) repeats the whole array n times: [1, 2] becomes [1, 2, 1, 2].

np.repeat(a, n) repeats each element n times: [1, 2] becomes [1, 1, 2, 2].

tile accepts a tuple to repeat along several axes, which is how you build a checkerboard or replicate a small pattern across a grid.

repeat accepts an axis and a per-element count, so np.repeat(rows, counts, axis=0) expands a table according to a count column — the standard way to turn aggregated data back into one row per observation.

Neither is a substitute for broadcasting. If you are tiling an array purely so that its shape matches another one for an arithmetic operation, broadcasting will do it without allocating anything, and the tile is wasted memory.

Padding

np.pad(a, width, mode) adds elements around the edges.

width can be a single number, a pair for before and after, or a pair per axis. The nesting gets confusing quickly, and it is worth checking the result's shape the first time on any new call.

The mode argument covers the cases that would otherwise be fiddly: "constant" with a constant_values argument, "edge" to repeat the border, "reflect" and "symmetric" to mirror, and "wrap" to tile.

This is how you implement boundary conditions for a convolution or a stencil without writing an index-clamping branch, and it is one of the functions that quietly removes a lot of code.

block, for nested assembly

np.block builds an array from a nested list of arrays, laid out the way the nesting reads.

np.block([[A, B],
          [C, D]])

produces the block matrix, provided the pieces have compatible shapes. It is far clearer than the equivalent chain of hstack inside vstack, and it is the right tool for assembling a matrix from named submatrices — a covariance built from blocks, a system of equations assembled from parts.

Choosing among the joins

The functions overlap enough to be worth a summary.

concatenate — the general form. Explicit axis, works on any dimensionality, no surprises. Reach for this when the dimensionality might vary or when clarity matters.

stack — when a new axis is what you want. Collecting frames, samples or repeated runs into a batch.

vstack / hstack / dstack — conveniences with a fixed axis. Safe on 2-D, and divergent on 1-D in a way that catches people. Fine when you know the shapes exactly.

column_stack — turns 1-D arrays into columns, which is what people usually want from hstack and do not get.

block — nested assembly from named pieces.

r_ / c_ — terse, for interactive use.

And the rule that applies to all of them: none belongs inside a loop. Collect into a list, join once.

Splitting, in practice

The pieces returned by split are views, so splitting a large array is free and writing into a piece writes into the original. That is usually convenient and occasionally surprising, and it follows the same rules as any other slice.

array_split distributes an uneven remainder across the first pieces, so splitting 10 into 3 gives lengths 4, 3, 3. That is deterministic, which matters if two processes split the same data independently and must agree.

For chunking work rather than dividing it evenly, computing the boundaries yourself and passing them as a list is often clearer than asking for a count — np.split(a, range(0, len(a), chunk)) cuts at fixed intervals regardless of whether the total divides evenly.

Joining arrays of different dtypes

Concatenating an integer array with a float array gives a float result, following the same promotion rules as arithmetic.

That is usually what you want, and occasionally not. Joining an int64 array of identifiers with a float64 array of measurements produces a float array in which large identifiers may no longer be exactly representable.

dtype= on concatenate forces the result type explicitly, and casting="no" makes a mismatch an error rather than a silent promotion. In code where the dtype matters, being explicit costs one argument and removes a whole category of surprise.

Joining a fixed-width string array with a longer one widens to fit, which is the one place NumPy strings do the accommodating thing rather than truncating.

Building an output array up front

When the final size is known, neither joining nor appending is necessary.

out = np.empty((n_rows, n_cols))
for i, row in enumerate(source):
    out[i] = compute(row)

One allocation, no copying, and the slices being assigned into are views. This is the fastest form when a loop is unavoidable, and it is clearer than accumulating pieces to join later.

np.empty is right here because every element is written. If some rows might be skipped, np.zeros or np.full(shape, np.nan) makes the unfilled entries visible rather than leaving whatever was in memory.

Preallocating also forces you to state the output shape, which frequently surfaces an error in the reasoning before any code runs.

Splitting for parallel work

np.array_split(a, n) is the natural way to divide data among n workers. It handles the case where the length does not divide evenly, distributing the remainder across the first pieces deterministically.

Because the pieces are views, splitting costs nothing — but views cannot cross a process boundary, so multiprocessing will copy them anyway when pickling. For thread-based parallelism the views are shared directly, which is one of the reasons threads are attractive for NumPy work.

For chunking rather than dividing — fixed-size pieces, however many that turns out to be — passing explicit boundaries is clearer:

np.split(a, range(chunk, len(a), chunk))

That cuts every chunk elements and leaves a shorter final piece, which is usually what "process in batches of 1000" means.

The summary

Joining along an existing axis: concatenate, with an explicit axis.

Adding a new axis: stack, with an explicit axis.

1-D arrays into columns: column_stack, not hstack.

Assembling from named blocks: block.

Duplicating: tile for the whole array, repeat for each element — and neither if broadcasting would do the job without allocating.

Adding borders: pad, with the mode chosen for the boundary condition.

Dividing: split for exact division, array_split for uneven, explicit boundaries for fixed-size chunks.

And the rule that overrides all of them: none of these belongs inside a loop that runs once per element. Collect into a Python list and join once, or preallocate and assign into slices.

A closing note

Joining arrays is one of the places where NumPy offers several functions for what feels like one job, and the abundance is more confusing than helpful at first.

The way through it is the question at the top of this module: does the result have the same number of dimensions as the inputs, or one more? Everything else is a convenience wrapper over that decision, and concatenate and stack with an explicit axis will do any of it correctly if you would rather not remember the rest.

Check yourself

0 of 4

Answer without scrolling back up.

  1. What is the difference between `concatenate` and `stack`?

  2. What does `np.hstack` do with two 1-D arrays of length 3?

  3. How does `split` differ from `array_split`?

  4. Why not concatenate inside a loop?

Cheat sheet

Stacking and Splitting

You have several arrays and want one. There are two distinct things that could mean, and NumPy gives them separate functions rather than guessing.

NUMPY · vizlearn.in/numpy/stacking_and_splitting.html

About the author

Ashish Jangra builds and maintains VizLearn. Every module here is written and the visualisation behind it hand-built, so the numbers in a readout come from the same code that draws the picture. Corrections are genuinely welcome and get priority over everything else — if a page states something wrong, or an animation misrepresents what the algorithm does, get in touch.