How to Flatten an Array in NumPy
Flattening arrays is one of those operations you'll perform hundreds of times in any data science or machine learning project. Whether you're preparing features for a model, serializing data for...
Key Insights
- Use
flatten()when you need a guaranteed copy that won’t affect the original array—it’s the safest choice for data preprocessing pipelines - Use
ravel()for memory-efficient flattening when you’re only reading the data, but be aware it returns a view that can mutate the source array - Use
reshape(-1)when you need flexibility in chaining operations or working with unknown dimensions—it behaves likeravel()but integrates better into reshape workflows
Flattening arrays is one of those operations you’ll perform hundreds of times in any data science or machine learning project. Whether you’re preparing features for a model, serializing data for storage, or converting image tensors into feature vectors, you need to collapse multi-dimensional arrays into a single dimension.
NumPy gives you three main approaches: flatten(), ravel(), and reshape(-1). They all produce a 1D array, but they differ in memory behavior, performance, and safety. Choosing the wrong one can introduce subtle bugs or tank your performance on large datasets.
Let’s start with what flattening actually looks like:
import numpy as np
# A 2D array (3x4 matrix)
arr_2d = np.array([[1, 2, 3, 4],
[5, 6, 7, 8],
[9, 10, 11, 12]])
# A 3D array (2x2x3 tensor)
arr_3d = np.array([[[1, 2, 3],
[4, 5, 6]],
[[7, 8, 9],
[10, 11, 12]]])
print(f"2D shape: {arr_2d.shape} -> Flattened: {arr_2d.flatten().shape}")
print(f"3D shape: {arr_3d.shape} -> Flattened: {arr_3d.flatten().shape}")
print(f"Flattened 2D: {arr_2d.flatten()}")
# Output:
# 2D shape: (3, 4) -> Flattened: (12,)
# 3D shape: (2, 2, 3) -> Flattened: (12,)
# Flattened 2D: [ 1 2 3 4 5 6 7 8 9 10 11 12]
Using ndarray.flatten()
The flatten() method is the most straightforward approach. It always returns a copy of the data, which means modifications to the flattened array never affect the original.
import numpy as np
arr = np.array([[1, 2, 3],
[4, 5, 6]])
# Default: C-order (row-major, reads across rows first)
flat_c = arr.flatten()
print(f"C-order (default): {flat_c}")
# Output: [1 2 3 4 5 6]
# F-order (column-major, reads down columns first)
flat_f = arr.flatten(order='F')
print(f"F-order: {flat_f}")
# Output: [1 4 2 5 3 6]
# A-order (preserves array's actual memory layout)
arr_fortran = np.array([[1, 2, 3], [4, 5, 6]], order='F')
flat_a = arr_fortran.flatten(order='A')
print(f"A-order on Fortran array: {flat_a}")
# Output: [1 4 2 5 3 6]
The order parameter matters when you’re interfacing with libraries that expect specific memory layouts. C-order (row-major) is the default in NumPy and most Python libraries. Fortran-order (column-major) is common in scientific computing libraries and MATLAB.
Here’s proof that flatten() creates a copy:
import numpy as np
original = np.array([[1, 2], [3, 4]])
flattened = original.flatten()
# Modify the flattened array
flattened[0] = 999
print(f"Original: {original}")
print(f"Flattened: {flattened}")
# Output:
# Original: [[1 2]
# [3 4]]
# Flattened: [999 2 3 4]
The original array stays intact. This safety comes at a cost—you’re allocating new memory and copying all the data.
Using ndarray.ravel()
The ravel() method is the performance-oriented sibling of flatten(). It returns a view of the original array when possible, avoiding memory allocation and data copying.
import numpy as np
arr = np.array([[1, 2, 3],
[4, 5, 6]])
# ravel() returns a view when the array is contiguous
raveled = arr.ravel()
print(f"Raveled: {raveled}")
print(f"Shares memory: {np.shares_memory(arr, raveled)}")
# Output:
# Raveled: [1 2 3 4 5 6]
# Shares memory: True
A view means the raveled array points to the same underlying memory as the original. No copying happens—it’s essentially free.
But ravel() can’t always return a view. When the array isn’t contiguous in memory (like after certain slicing operations), it must create a copy:
import numpy as np
arr = np.array([[1, 2, 3, 4],
[5, 6, 7, 8],
[9, 10, 11, 12]])
# Slice every other column - creates non-contiguous view
sliced = arr[:, ::2]
print(f"Sliced array:\n{sliced}")
raveled = sliced.ravel()
print(f"Shares memory after slicing: {np.shares_memory(sliced, raveled)}")
# Output:
# Sliced array:
# [[ 1 3]
# [ 5 7]
# [ 9 11]]
# Shares memory after slicing: False
The non-contiguous slice forces ravel() to copy. You can check if an array is contiguous with arr.flags['C_CONTIGUOUS'].
Using reshape(-1)
The reshape() method with -1 tells NumPy to infer the dimension size. When you pass -1 as the only dimension, you get a 1D array:
import numpy as np
arr = np.array([[[1, 2],
[3, 4]],
[[5, 6],
[7, 8]]])
print(f"Original shape: {arr.shape}")
reshaped = arr.reshape(-1)
print(f"Reshaped: {reshaped}")
print(f"Shares memory: {np.shares_memory(arr, reshaped)}")
# Output:
# Original shape: (2, 2, 2)
# Reshaped: [1 2 3 4 5 6 7 8]
# Shares memory: True
The -1 approach behaves like ravel()—it returns a view when possible. Its real advantage is flexibility in reshape chains:
import numpy as np
# When you don't know all dimensions upfront
data = np.random.rand(100, 32, 32, 3) # 100 images, 32x32, RGB
# Flatten each image while keeping batch dimension
features = data.reshape(data.shape[0], -1)
print(f"Feature matrix shape: {features.shape}")
# Output: Feature matrix shape: (100, 3072)
# Completely flatten
all_pixels = data.reshape(-1)
print(f"All pixels: {all_pixels.shape}")
# Output: All pixels: (307200,)
Performance Comparison
Let’s benchmark these methods on realistic data sizes:
import numpy as np
import time
def benchmark(func, arr, iterations=1000):
start = time.perf_counter()
for _ in range(iterations):
_ = func(arr)
return (time.perf_counter() - start) / iterations * 1_000_000 # microseconds
# Test on a large array
large_arr = np.random.rand(1000, 1000)
flatten_time = benchmark(lambda x: x.flatten(), large_arr)
ravel_time = benchmark(lambda x: x.ravel(), large_arr)
reshape_time = benchmark(lambda x: x.reshape(-1), large_arr)
print(f"flatten(): {flatten_time:.2f} µs")
print(f"ravel(): {ravel_time:.2f} µs")
print(f"reshape(): {reshape_time:.2f} µs")
# Typical output:
# flatten(): 485.23 µs
# ravel(): 0.52 µs
# reshape(): 0.48 µs
The difference is dramatic. flatten() is roughly 1000x slower because it allocates memory and copies a million floats. ravel() and reshape(-1) just create a new array object pointing to existing memory.
For a single operation, 500 microseconds is nothing. But in a training loop processing millions of samples, these costs compound. Choose wisely.
Common Pitfalls
The biggest trap with ravel() and reshape(-1) is unintended mutation. When they return a view, modifying the result changes the original:
import numpy as np
# Preprocessing pipeline - WRONG
def preprocess_wrong(image):
flat = image.ravel()
flat -= flat.mean() # Normalize - but this modifies the original!
flat /= flat.std()
return flat
original_image = np.array([[100, 150],
[200, 250]], dtype=float)
backup = original_image.copy()
processed = preprocess_wrong(original_image)
print(f"Original changed: {not np.array_equal(original_image, backup)}")
# Output: Original changed: True
The fix is simple—use flatten() when you intend to modify the result:
import numpy as np
# Preprocessing pipeline - CORRECT
def preprocess_correct(image):
flat = image.flatten() # Safe copy
flat -= flat.mean()
flat /= flat.std()
return flat
Another pitfall is order mismatches. If you flatten with C-order but your downstream code expects Fortran-order, you’ll get scrambled data:
import numpy as np
arr = np.array([[1, 2],
[3, 4]])
# Simulating data from a Fortran library
fortran_flat = arr.flatten(order='F') # [1, 3, 2, 4]
# Reconstructing with wrong assumption
wrong_reshape = fortran_flat.reshape(2, 2) # Assumes C-order
print(f"Wrong reconstruction:\n{wrong_reshape}")
# Output:
# [[1 3]
# [2 4]] # Transposed!
# Correct reconstruction
correct_reshape = fortran_flat.reshape(2, 2, order='F')
print(f"Correct reconstruction:\n{correct_reshape}")
# Output:
# [[1 2]
# [3 4]]
Conclusion
Here’s your quick reference for flattening NumPy arrays:
Use flatten() when safety matters more than speed. It always copies, so your original data stays protected. This is the right choice for preprocessing pipelines where you’ll modify the flattened data.
Use ravel() when you’re reading the flattened data without modification, especially in performance-critical code. Just remember it might return a view that shares memory with the original.
Use reshape(-1) when you’re working with dynamic shapes or chaining reshape operations. It behaves like ravel() but fits naturally into reshape workflows where you’re inferring dimensions.
For most everyday work, flatten() is the safest default. Optimize to ravel() or reshape(-1) when profiling shows flattening is a bottleneck—and only after you’ve verified you won’t accidentally mutate your source data.