If you work with numbers in Python — sales figures, sensor readings, survey results, images, or machine learning data — you’ll use NumPy, whether you realize it or not. It’s the foundation of Python’s entire data ecosystem: Pandas is built on top of it, Matplotlib plots its arrays, and libraries like scikit-learn, SciPy, and PyTorch all speak its language.

NumPy gives Python one powerful new data type, the array, and a huge library of fast functions to work with it. With a few lines of NumPy, you can do calculations on millions of numbers at once — typically dozens of times faster than with regular Python lists.

This guide teaches NumPy from the ground up: how arrays work, how to create, select, and filter data, how to do math on whole arrays at once, and two ideas that make NumPy click — axes and broadcasting. It finishes with real-world examples and the mistakes that trip up most beginners. Every example shows its real output, so you can follow along and check your results.

What Is NumPy?

NumPy (short for “Numerical Python”) is a free, open-source library for working with numbers. Its core is the ndarray (n-dimensional array): a grid of values, all of the same type, stored efficiently in memory.

That one design decision — every value in an array has the same type, stored side by side — is what makes NumPy fast. A Python list can hold anything (numbers, text, other lists), so Python has to check each item’s type every time it does something with it. A NumPy array knows all of its values are, say, 64-bit decimal numbers, so its operations run in fast, compiled code instead of a slow Python loop.

Installing and Importing NumPy

Install NumPy in a virtual environment (or with uv). If you’ve installed Pandas, you already have it.

pip install numpy

By convention, NumPy is always imported as np:

import numpy as np

prices = np.array([19.99, 49.00, 9.99, 300.00, 1200.00])
print(prices)
print(prices * 1.0775)
[  19.99   49.      9.99  300.   1200.  ]
[  21.539225   52.7975     10.764225  323.25     1293.      ]

That second line is the heart of NumPy: multiplying the array by 1.0775 applies 7.75% tax to every price at once — no loop needed.

Versions: the output in this guide is from NumPy 2.5, and every example produces identical output on NumPy 1.26.

Arrays vs. Python Lists

Arrays and lists look similar, but they behave very differently. Compare them side by side:

prices_list = [19.99, 49.00, 9.99, 300.00, 1200.00]
print([p * 2 for p in prices_list])
print(prices * 2)
print(prices_list + prices_list)
print(prices + prices)
[39.98, 98.0, 19.98, 600.0, 2400.0]
[  39.98   98.     19.98  600.   2400.  ]
[19.99, 49.0, 9.99, 300.0, 1200.0, 19.99, 49.0, 9.99, 300.0, 1200.0]
[  39.98   98.     19.98  600.   2400.  ]
  • To double every number in a list, you need a loop or a list comprehension (see Python Basics: Lists and Dictionaries). With an array, prices * 2 does it directly.
  • For lists, + means join: the result is a list twice as long. For arrays, + means add, element by element.

Applying an operation to every element at once is called vectorization, and it’s not just shorter — it’s much faster. Here’s a quick timing test you can run yourself, doubling a million numbers:

import timeit

setup = "import numpy as np; data = list(range(1_000_000)); arr = np.arange(1_000_000)"
t_list = min(timeit.repeat("[x * 2 for x in data]", setup=setup, number=10, repeat=5)) / 10
t_np = min(timeit.repeat("arr * 2", setup=setup, number=10, repeat=5)) / 10
print(f"List: {t_list*1000:.1f} ms")
print(f"NumPy: {t_np*1000:.2f} ms")
print(f"NumPy is about {t_list/t_np:.0f}x faster")

On my computer, it printed:

List: 58.1 ms
NumPy: 1.28 ms
NumPy is about 45x faster

Your exact numbers will differ, but NumPy is typically tens of times faster for this kind of work — and the gap grows with more complex calculations.

Creating Arrays

Besides converting a list with np.array(), NumPy has functions to create common arrays directly:

print(np.zeros(5))
print(np.ones((2, 3)))
print(np.full(3, 7))
print(np.arange(0, 10, 2))
print(np.linspace(0, 1, 5))
[0. 0. 0. 0. 0.]
[[1. 1. 1.]
 [1. 1. 1.]]
[7 7 7]
[0 2 4 6 8]
[0.   0.25 0.5  0.75 1.  ]
  • np.zeros() and np.ones() create arrays filled with 0s or 1s. Pass a tuple like (2, 3) for 2 rows and 3 columns.
  • np.full() fills an array with any value.
  • np.arange(start, stop, step) works like Python’s range(), but returns an array.
  • np.linspace(start, stop, count) creates evenly spaced values, including the end point. Use it when you know how many values you want rather than the step size.

Random Numbers

For simulations, testing, and sample data, use a random number generator. Giving it a seed makes the “random” numbers the same every time you run the code, so your results are reproducible:

rng = np.random.default_rng(seed=42)
print(rng.integers(1, 7, size=10))
print(rng.random(3).round(3))
print(rng.normal(loc=100, scale=15, size=4).round(1))
[1 5 4 3 3 6 1 5 2 1]
[0.976 0.761 0.786]
[ 99.7  87.2 113.2 111.7]
  • rng.integers(1, 7, size=10) rolls ten six-sided dice (the upper limit, 7, isn’t included).
  • rng.random() returns decimals between 0 and 1.
  • rng.normal() draws from a bell curve — here with an average of 100 and a standard deviation of 15.

With the same seed (42), you’ll get exactly these numbers. default_rng() is the modern way to generate random numbers; older tutorials use np.random.seed() and np.random.randint(), which still work but aren’t recommended for new code.

The Example Dataset: Store Sales

For most of this guide, we’ll use daily sales for three stores — Downtown, Airport, and Mall — over five days, Monday to Friday. Each row is a store, and each column is a day:

sales = np.array([
    [120, 135, 150, 110, 190],
    [ 80,  95,  70,  85, 130],
    [200, 210, 180, 220, 260],
])
print(sales.shape)
print(sales.ndim)
print(sales.size)
print(sales.dtype)
(3, 5)
2
15
int64

These four attributes describe any array:

  • shape — the size of each dimension: 3 rows and 5 columns.
  • ndim — the number of dimensions: 2, because it’s a table.
  • size — the total number of values: 15.
  • dtype — the data type of every value: int64, a 64-bit integer.

Data Types (dtype)

NumPy picks a data type automatically based on your values, but you can also set it yourself:

print(np.array([1, 2, 3]).dtype)
print(np.array([1, 2, 3.5]).dtype)
print(np.array([1, 2, 3], dtype=np.float32).dtype)
print(np.array(["a", "b"]).dtype)
int64
float64
float32
<U1

Because every value in an array must have the same type, one decimal number turns the whole array into floats (float64). Smaller types like float32 or int8 use less memory, which matters for very large arrays. <U1 means Unicode text of length 1 — NumPy can store text, but for text-heavy data, Pandas is the better tool. (On Windows with NumPy 1.x, the default integer type is int32 instead of int64.)

Indexing and Slicing

One-Dimensional Arrays

Indexing and slicing a 1D array works just like a Python list:

x = np.array([10, 20, 30, 40, 50])
print(x[0], x[-1])
print(x[1:4])
print(x[::2])
10 50
[20 30 40]
[10 30 50]

Two-Dimensional Arrays

For a 2D array, give the row first and the column second, separated by a comma:

print(sales[0])
print(sales[0, 4])
print(sales[:, 4])
print(sales[1:, :2])
[120 135 150 110 190]
190
[190 130 260]
[[ 80  95]
 [200 210]]
  • sales[0] is the first row: all of Downtown’s sales.
  • sales[0, 4] is one value: Downtown on Friday.
  • sales[:, 4] uses : to mean “all rows,” giving the fourth column: every store’s Friday sales.
  • sales[1:, :2] combines two slices: the Airport and Mall rows, for Monday and Tuesday.

Views vs. Copies

This is one of the most important differences from Python lists. Slicing an array doesn’t copy the data — it creates a view, a window into the same memory. Changing the view changes the original:

first_two = x[:2]
first_two[0] = 999
print(x)

x = np.array([10, 20, 30, 40, 50])
safe = x[:2].copy()
safe[0] = 999
print(x)
[999  20  30  40  50]
[10 20 30 40 50]

Views make NumPy fast and memory-efficient, because slicing a huge array doesn’t duplicate it. But if you want to change a slice without affecting the original, call .copy().

Filtering with Boolean Masks

A comparison on an array returns an array of True and False values, called a boolean mask:

print(sales > 150)
print(sales[sales > 150])
print((sales > 150).sum())
print(sales[(sales >= 100) & (sales <= 130)])
[[False False False False  True]
 [False False False False False]
 [ True  True  True  True  True]]
[190 200 210 180 220 260]
6
[120 110 130]
  • Putting a mask inside square brackets keeps only the values where the mask is True.
  • Because True counts as 1 and False as 0, .sum() on a mask counts the matches: 6 days had sales over 150.
  • Combine conditions with & (and), | (or), and ~ (not), with parentheses around each condition. Python’s and and or don’t work on arrays.

Choosing Values with np.where()

np.where(condition, value_if_true, value_if_false) works like an if statement applied to every element:

print(np.where(sales > 150, "high", "normal"))
print(np.where(sales < 100, 100, sales))
[['normal' 'normal' 'normal' 'normal' 'high']
 ['normal' 'normal' 'normal' 'normal' 'normal']
 ['high' 'high' 'high' 'high' 'high']]
[[120 135 150 110 190]
 [100 100 100 100 130]
 [200 210 180 220 260]]

The second example raises every value below 100 up to 100, and keeps everything else as it is.

Selecting with Lists of Indexes

You can also pass a list of positions to pick rows (or values) in any order. This is called fancy indexing, and it’s handy for looking up labels that match your data:

stores = np.array(["Downtown", "Airport", "Mall"])
print(sales[[2, 0]])
print(stores[[2, 0]])
[[200 210 180 220 260]
 [120 135 150 110 190]]
['Mall' 'Downtown']

Vectorized Math

Every arithmetic operator works element by element, and NumPy includes hundreds of universal functions (ufuncs) for math:

celsius = np.array([-10, 0, 20, 37])
print(celsius * 9 / 5 + 32)
print(np.sqrt([4, 9, 16]))
print(np.round(np.array([1.234, 5.678]), 1))
[14.  32.  68.  98.6]
[2. 3. 4.]
[1.2 5.7]

The whole Celsius-to-Fahrenheit formula runs on every temperature at once. Other common functions include np.abs(), np.exp(), np.log(), np.sin(), and np.floor().

Aggregations and the axis Parameter

Aggregations reduce many values to one:

print(sales.sum())
print(sales.mean().round(2))
print(sales.max(), sales.min())
print(sales.std().round(2))
2235
149.0
260 70
56.13

These summarize all 15 values: total sales of 2,235, an average day of 149, a best day of 260, a worst day of 70, and a standard deviation (how spread out the values are) of 56.13.

Understanding axis

Usually you want totals per store or per day, not for everything. That’s what the axis parameter is for, and it’s the concept beginners find most confusing. Here’s the simplest way to think about it: axis is the direction that gets collapsed.

              axis=1 → (across the columns)
              Mon  Tue  Wed  Thu  Fri
Downtown    [ 120  135  150  110  190 ]   sum(axis=1) → one total per store
Airport     [  80   95   70   85  130 ]
Mall        [ 200  210  180  220  260 ]
   ↓
axis=0 (down the rows) → sum(axis=0) gives one total per day
print(sales.sum(axis=1))
print(sales.sum(axis=0))
print(sales.mean(axis=0))
[ 705  460 1070]
[400 440 400 415 580]
[133.33333333 146.66666667 133.33333333 138.33333333 193.33333333]
  • axis=1 collapses the columns, leaving one value per row: each store’s weekly total. The Mall sold 1,070.
  • axis=0 collapses the rows, leaving one value per column: each day’s total across all stores. Friday was the best day, with 580.

Finding Where Things Are: argmax and cumsum

print(stores[sales.sum(axis=1).argmax()])
print(sales.argmax(axis=1))
print(np.cumsum(sales[0]))
Mall
[4 4 4]
[120 255 405 515 705]
  • argmax() returns the position of the largest value instead of the value itself. Combined with the stores array, it answers “which store sold the most?” in one line.
  • sales.argmax(axis=1) finds each store’s best day: position 4 (Friday) for all three.
  • np.cumsum() returns a running total: Downtown had sold 405 by Wednesday and 705 by Friday.

Broadcasting

Broadcasting is how NumPy does math between arrays of different shapes. It’s what makes many calculations possible in a single line.

An Array and a Single Number

The simplest case: a single number is applied to every element.

print(sales * 2)
[[240 270 300 220 380]
 [160 190 140 170 260]
 [400 420 360 440 520]]

An Array and a Row

Add a 1D array with 5 values to our 3×5 table, and NumPy stretches the row down so it’s added to every store. Here, a bonus is added for Thursday and Friday promotions:

day_bonus = np.array([0, 0, 0, 10, 25])
print(sales + day_bonus)
[[120 135 150 120 215]
 [ 80  95  70  95 155]
 [200 210 180 230 285]]

An Array and a Column

To apply a different value to each row, use a column — an array shaped (3, 1). Here, each store’s sales are divided by its own daily target to see how close it came:

targets = np.array([[150], [100], [200]])
print((sales / targets).round(2))
[[0.8  0.9  1.   0.73 1.27]
 [0.8  0.95 0.7  0.85 1.3 ]
 [1.   1.05 0.9  1.1  1.3 ]]

A result of 1.27 means 127% of the target — every store beat its target on Friday.

The Broadcasting Rule

NumPy compares the two shapes from the right. Two dimensions are compatible if they’re equal, or if one of them is 1 (in which case it’s stretched). Missing dimensions on the left count as 1.

ShapesCompatible?Why
(3, 5) and a single numberYesA single number works with any shape
(3, 5) and (5,)YesThe last dimensions match (5 and 5)
(3, 5) and (3, 1)Yes5 vs. 1: the 1 is stretched; 3 vs. 3 match
(3, 5) and (3,)No5 vs. 3 don’t match, and neither is 1

That last case is a common mistake — trying to add one value per store using a flat array:

try:
    sales + np.array([1, 2, 3])
except ValueError as error:
    print("ValueError:", error)
ValueError: operands could not be broadcast together with shapes (3,5) (3,)

The fix is to make it a column, either with np.array([[1], [2], [3]]) or with np.array([1, 2, 3])[:, np.newaxis], which reshapes it to (3, 1).

Putting Broadcasting to Work: Normalizing Data

Broadcasting and axes come together in a very common task: standardizing data into z-scores, which show how far each value is from its row’s average in standard deviations. Here, it answers “which days were unusually good or bad for each store,” even though the stores have very different sales levels:

store_mean = sales.mean(axis=1, keepdims=True)
store_std = sales.std(axis=1, keepdims=True)
z = (sales - store_mean) / store_std
print(z.round(2))
[[-0.75 -0.21  0.32 -1.11  1.75]
 [-0.58  0.15 -1.07 -0.34  1.84]
 [-0.53 -0.15 -1.28  0.23  1.73]]

keepdims=True keeps the results as columns, shaped (3, 1), so they broadcast correctly against the (3, 5) table. The result shows Friday was well above normal for every store (around +1.7 to +1.8), while Thursday was Downtown’s weakest day.

Reshaping and Combining Arrays

Changing the Shape

y = np.arange(12)
print(y.reshape(3, 4))
print(y.reshape(2, -1))
print(sales.T)
print(sales.flatten()[:6])
[[ 0  1  2  3]
 [ 4  5  6  7]
 [ 8  9 10 11]]
[[ 0  1  2  3  4  5]
 [ 6  7  8  9 10 11]]
[[120  80 200]
 [135  95 210]
 [150  70 180]
 [110  85 220]
 [190 130 260]]
[120 135 150 110 190  80]
  • reshape() rearranges the same values into a new shape. The total number of values must stay the same: 12 values can be 3×4 or 2×6, but not 5×5.
  • -1 means “figure out this dimension for me.”
  • .T transposes the array, swapping rows and columns: now each row is a day.
  • .flatten() turns any array back into one dimension.

Stacking Arrays Together

week2 = sales + 10
print(np.vstack([sales, week2]).shape)
print(np.hstack([sales, week2]).shape)
print(np.concatenate([sales, week2], axis=0).shape)
(6, 5)
(3, 10)
(6, 5)

np.vstack() stacks arrays vertically (more rows), np.hstack() stacks them horizontally (more columns), and np.concatenate() does either, depending on the axis.

Common Pitfalls

Missing Values (NaN)

NumPy represents missing or undefined numbers with np.nan (“not a number”). A single nan makes regular aggregations return nan:

readings = np.array([22.5, 23.1, np.nan, 24.0])
print(readings.mean())
print(np.nanmean(readings))
print(np.isnan(readings))
nan
23.2
[False False  True False]

Use the nan-safe versions — np.nanmean(), np.nansum(), np.nanmax(), and so on — to ignore missing values, and np.isnan() to find them. (Never use == np.nan to check for missing values: nan isn’t equal to anything, not even itself.)

Comparing Decimal Numbers

As you saw in Python Basics: Variables and Data Types, decimal math isn’t perfectly precise:

print(0.1 + 0.2 == 0.3)
print(np.isclose(0.1 + 0.2, 0.3))
False
True

Use np.isclose() (or np.allclose() for whole arrays) to compare floats.

Integer Overflow

Python’s own integers can grow as large as needed, but NumPy’s integers have a fixed size. If a result doesn’t fit, it wraps around — silently:

small = np.array([100, 120], dtype=np.int8)
print(small + small)
[-56 -16]

An int8 can only hold values from -128 to 127, so 100 + 100 wrapped around to -56, with no error. The default int64 can hold numbers up to about 9.2 quintillion, so you’ll rarely hit this unless you choose a small type or work with image data (below).

Real-World Examples

Matrix Math and Solving Equations

NumPy distinguishes element-wise multiplication (*) from matrix multiplication (@):

a = np.array([[1, 2], [3, 4]])
b = np.array([[5, 6], [7, 8]])
print(a * b)
print(a @ b)
[[ 5 12]
 [21 32]]
[[19 22]
 [43 50]]

The np.linalg module solves linear algebra problems. Say three coffees and two pastries cost $70 for a group, and one coffee and four pastries cost $90 for another. What does each item cost?

coefficients = np.array([[3, 2], [1, 4]])
totals = np.array([70, 90])
solution = np.linalg.solve(coefficients, totals)
print(solution)
[10. 20.]

A coffee costs $10 and a pastry costs $20. (Check: 3 × 10 + 2 × 20 = 70, and 10 + 4 × 20 = 90.)

A Quick Simulation: Rolling Dice

Simulations are a classic NumPy use case. What’s the most likely total when you roll two dice? Instead of working out the math, roll them 100,000 times:

rng = np.random.default_rng(seed=7)
rolls = rng.integers(1, 7, size=(100_000, 2))
totals = rolls.sum(axis=1)
counts = np.bincount(totals, minlength=13)[2:]
print(counts)
print((counts / len(totals)).round(3))
print(np.argmax(counts) + 2)
[ 2870  5457  8187 11046 13949 16663 13906 11266  8350  5535  2771]
[0.029 0.055 0.082 0.11  0.139 0.167 0.139 0.113 0.084 0.055 0.028]
7

np.bincount() counts how many times each total appeared. The counts are for totals 2 through 12, and they form the familiar pyramid shape: 7 is the most common total, appearing about 16.7% of the time — very close to the exact probability of 1 in 6. All 200,000 dice rolls and the counting took a fraction of a second.

Moving Averages

A moving average smooths out day-to-day noise to show a trend. Here’s a 3-day moving average:

daily = np.array([120, 135, 150, 110, 190, 160, 175, 140])
window = 3
moving = np.convolve(daily, np.ones(window) / window, mode="valid")
print(moving.round(1))
[135.  131.7 150.  153.3 175.  158.3]

Each value is the average of three consecutive days: (120 + 135 + 150) ÷ 3 = 135, and so on. (For data with dates, Pandas’ rolling() is often more convenient.)

Images Are Arrays

A grayscale digital image is just a 2D array of brightness values from 0 (black) to 255 (white), usually stored as uint8 (unsigned 8-bit integers). That means image editing is array math:

image = np.array([
    [ 10,  50,  90],
    [130, 170, 210],
    [250, 200, 150],
], dtype=np.uint8)
print(255 - image)
print(np.clip(image.astype(np.int16) + 80, 0, 255).astype(np.uint8))
[[245 205 165]
 [125  85  45]
 [  5  55 105]]
[[ 90 130 170]
 [210 250 255]
 [255 255 230]]

255 - image creates a negative. Brightening is trickier because of overflow: adding 80 to 250 would wrap around in uint8. So the image is first converted to a larger type (int16), brightened, then clipped to the valid range with np.clip() before converting back. A color image works the same way, with a third dimension for the red, green, and blue channels.

Saving and Loading Arrays

np.save("sales.npy", sales)
loaded = np.load("sales.npy")
print(np.array_equal(sales, loaded))
np.savetxt("sales.csv", sales, delimiter=",", fmt="%d")
print(np.loadtxt("sales.csv", delimiter=",", dtype=int)[0])
True
[120 135 150 110 190]

.npy is NumPy’s own fast, exact binary format — ideal for saving arrays between your own scripts. np.savetxt() and np.loadtxt() read and write plain text files like CSV, which other programs can open.

NumPy and Pandas

Pandas is built on NumPy: each column of a DataFrame is backed by a NumPy array. You can move between them easily:

  • df["col"].to_numpy() turns a Pandas column into a NumPy array.
  • pd.DataFrame(array, columns=[...]) turns an array into a DataFrame.
  • NumPy functions like np.where() and np.log() work directly on Pandas columns.

Use NumPy for fast math on numbers — matrices, simulations, images, and scientific calculations. Use Pandas when your data has labeled columns of different types, dates, or missing values to clean, as in Data Cleaning with Python Pandas. Most data projects use both. To visualize the results, see Python Data Visualization.

Common NumPy Mistakes

  • Using and/or instead of &/|. Python’s and and or don’t work on arrays. Use &, |, and ~, with parentheses around each condition.
  • Forgetting that slices are views. Changing a slice changes the original array. Use .copy() when you need an independent array.
  • Confusing * and @. * multiplies element by element; @ is matrix multiplication.
  • Mixing up axis=0 and axis=1. Remember: axis is the direction that gets collapsed. axis=0 gives one result per column; axis=1 gives one result per row.
  • Broadcasting a row when you meant a column. A shape (3,) array lines up with columns, not rows. Use shape (3, 1) — or keepdims=True — to apply one value per row.
  • Ignoring nan. One missing value makes mean() return nan. Use np.nanmean() and friends.
  • Overflowing small integer types, especially with images. Convert to a larger type before doing math, then clip.
  • Looping over arrays. If you’re writing a for loop over a NumPy array, there’s almost always a faster vectorized way.

NumPy Cheat Sheet

TaskCode
Create from a listnp.array([1, 2, 3])
Zeros, ones, a fixed valuenp.zeros(5), np.ones((2, 3)), np.full(3, 7)
Rangesnp.arange(0, 10, 2), np.linspace(0, 1, 5)
Random numbersrng = np.random.default_rng(42), rng.integers(1, 7, 10), rng.normal(0, 1, 5)
Shape and typea.shape, a.ndim, a.size, a.dtype
Selecta[0], a[1:4], m[row, col], m[:, 2]
Filtera[a > 5], a[(a > 1) & (a < 5)]
Conditional valuesnp.where(a > 5, "high", "low")
Aggregatea.sum(), a.mean(), a.max(), a.std()
Per row / per columnm.sum(axis=1), m.sum(axis=0)
Position of maxa.argmax()
Running totalnp.cumsum(a)
Reshapea.reshape(3, 4), a.reshape(2, -1), m.T, a.flatten()
Combinenp.vstack([a, b]), np.hstack([a, b]), np.concatenate([a, b])
Copyb = a.copy()
Missing valuesnp.isnan(a), np.nanmean(a)
Compare floatsnp.isclose(a, b)
Matrix multiplicationa @ b
Solve equationsnp.linalg.solve(A, b)
Save and loadnp.save("a.npy", a), np.load("a.npy")

Download the NumPy Cheat Sheet (PNG)

Conclusion

NumPy turns Python into a fast tool for working with numbers. Its arrays store values of one type efficiently, so vectorized operations run on millions of values at once, dozens of times faster than loops. You can create arrays from lists or with functions like arange() and linspace(); select data with indexing, slicing, and boolean masks; summarize it with aggregations along an axis; and combine arrays of different shapes with broadcasting.

Keep the pitfalls in mind — slices are views, nan spreads through calculations, decimals need np.isclose(), and small integer types can overflow — and you’ll have the foundation that Pandas, Matplotlib, scikit-learn, and the rest of Python’s data ecosystem are built on.