AlgoMaster Logo

NumPy Basics

Medium Priority16 min readUpdated June 6, 2026
Listen to this chapter
Unlock Audio

NumPy is the foundation that the rest of scientific Python is built on. Pandas, scikit-learn, SciPy, PyTorch, TensorFlow, and every plotting library that draws a curve either wrap NumPy arrays or use the same memory layout. Any numeric work in Python beyond a small list of prices eventually touches NumPy. This lesson covers what NumPy is, why it's faster than a Python list of numbers, how to create arrays, and the attributes to inspect on every array.

Why NumPy Exists

A Python list is a flexible container. It can hold an integer, a string, a customer object, and another list, all in the same sequence. That flexibility has a cost. Every item in a Python list is a full Python object: an integer like 42 is a PyLongObject with a reference count, a type pointer, and the actual value, sitting somewhere on the heap. The list itself is an array of pointers to those scattered objects.

When sum(prices) runs on a list of one million prices, Python walks one million pointers, dereferences each one to find the actual float object, unboxes the C double inside, adds it to a running total, and boxes the result back into a PyFloat so it can continue. The arithmetic is fast. The boxing, unboxing, pointer chasing, and per-item type checking is what eats the time.

NumPy throws all of that out. An array of one million prices in NumPy is one contiguous block of memory holding one million raw double values, packed end to end. No pointers, no per-item objects, no type checks per addition. prices.sum() hands the block to a C loop that adds doubles to a register. The whole operation runs at the speed of native C, not the speed of Python's interpreter.

The speedup is usually 10x to 100x for arithmetic-heavy work, sometimes more. That's not a micro-optimization, that's the difference between a script finishing overnight and a script finishing during a coffee break.

The list version scatters the values across the heap behind a layer of pointers. The array packs the same four numbers into a single tight block. The CPU benefits from the array because each value sits exactly where the one before it ended, so the prefetcher and the cache work efficiently.

Two other constraints come with the contiguous layout. First, every value in a NumPy array has the same type, which is what makes the packing possible. An int and a string cannot share the same array. Second, the size is fixed at creation time. Appending to a NumPy array means allocating a brand new block, copying everything over, and freeing the old one. Lists were designed for that pattern, arrays were not.

That sounds like a downside, and for some workloads it is. NumPy isn't meant to replace lists everywhere. It fits the work of doing math on a lot of numbers, which is most of data science, machine learning, scientific computing, and any analytics dashboard that has to crunch order data for a million customers a night.

Installing NumPy

NumPy is not part of the Python standard library. Install it with pip:

Anaconda and other scientific Python distributions already include NumPy. Once installed, the standard import is numpy as np. The convention is universal across tutorials, documentation, and Stack Overflow answers, so following it keeps other code readable.

The reported version depends on what pip pulled down. Anything in the 1.x or 2.x range works for the basics this lesson covers. NumPy 2.0 (released mid-2024) introduced some breaking changes for advanced users but nothing in the basics moves.

Creating Arrays

The most direct way to make an array is np.array, which takes any sequence of numbers and turns it into a NumPy array. A flat list produces a 1D array, a list of lists produces a 2D array, and so on.

The returned value is an ndarray, which is NumPy's array type. The repr drops the commas in a list and prints the values separated by spaces. Same data, different container.

A 2D array models tabular data. It is a matrix of rows and columns: each row is a record, each column is a field. Below is a small "orders" table where each row is (order_id, quantity, total):

The array printed as a 3-by-3 grid because that's its shape, and the values are in scientific notation because NumPy chose float64 as the dtype. Keeping the IDs and quantities as integers requires asking for an int array.

np.array is fine when the data is already in hand. Often it isn't; the goal is a block of a specific size filled with a specific value. NumPy has dedicated constructors for that.

Filled Arrays: zeros, ones, full

np.zeros(shape) allocates an array of the given shape filled with zeros. The shape can be an integer for a 1D array or a tuple for higher dimensions.

The default dtype is float64, which is why the zeros print with a trailing dot. Pass dtype=int for integer zeros:

np.ones(shape) is the mirror image: an array of the given shape filled with ones. It seeds a multiplier, a mask, or any "default to one" pattern.

For any other fill value, use np.full(shape, value):

np.zeros and np.ones allocate and initialize the whole block. For very large arrays where every value is overwritten before being read, np.empty(shape) allocates without initializing, which is faster but leaves whatever bytes happened to be there. Use np.empty only when every cell is guaranteed to be written.

Ranged Arrays: arange and linspace

For a sequence of evenly spaced values, there are two builders. np.arange(start, stop, step) works like Python's built-in range: it produces values starting at start, stepping by step, stopping before stop.

np.arange works fine with integer steps. With floating-point steps it becomes jittery because of float rounding: np.arange(0, 1, 0.1) doesn't always return exactly ten elements. For evenly spaced floats, use np.linspace(start, stop, num), which asks for the number of points rather than the step, and includes both endpoints by default.

Eleven values, the first is exactly 0.0, the last is exactly 0.5, and the step is computed automatically. linspace is the better choice when hitting exact endpoints matters, which covers most cases of generating plot axes, model parameters, or test inputs.

Random Arrays

For arrays of random values, the modern API is np.random.default_rng(), which returns a Generator object with methods on it. The older top-level functions like np.random.random still work but share global state, which is awkward for testing and reproducibility.

integers(low, high, size=n) follows the half-open convention: high is exclusive. So rng.integers(1, 6, size=4) produces values in [1, 2, 3, 4, 5]. rng.integers(1, 5) does not include 5, which is the common confusion.

Seeding the generator (seed=42) makes the sequence deterministic. Running the same code twice with the same seed produces the same numbers, which is the goal when writing tests or sharing reproducible examples.

Array Attributes

Every NumPy array carries metadata about itself. The five attributes that come up most are shape, dtype, ndim, size, and itemsize. None of them are methods; they are plain attributes, no parentheses.

AttributeMeaning
shapeTuple of dimension sizes. (3, 3) means 3 rows and 3 columns.
dtypeThe element type. float64 is a 64-bit floating point.
ndimNumber of dimensions. len(shape) returns the same number.
sizeTotal number of elements. Product of all values in shape.
itemsizeBytes per element. float64 uses 8 bytes, int32 uses 4 bytes.

Total memory used by the array's data buffer is size * itemsize. The orders array holds nine doubles, so 9 * 8 = 72 bytes plus a small fixed overhead for the array header. A million-row, three-column float64 array is 3,000,000 * 8 = 24 MB, a back-of-the-envelope calculation worth keeping at hand.

The dtype is what makes a NumPy array different from a list of numbers. Common dtypes:

DtypeBytesRange / precision
int81-128 to 127
int324About -2.1 billion to 2.1 billion
int648The default integer on most 64-bit systems
float324About 7 decimal digits of precision
float648About 15 decimal digits of precision, the default
bool1True or False

NumPy picks a dtype at array creation. For a list of integers it picks int64; for a list with any float in it, float64. The choice can be overridden with the dtype keyword:

For four elements the saving is meaningless. For a million-element array of small counts, going from int64 to int8 drops memory from 8 MB to 1 MB. That trade-off becomes relevant once arrays are large enough to matter.

Lists vs Arrays: A Concrete Comparison

The differences between a Python list and a NumPy array are easiest to see side by side. Same task, two implementations.

Doubling every price in a 1-million-element collection:

The exact numbers depend on the CPU, but the ratio matters. The list version walks a million Python objects, multiplying each by 2 in pure interpreter code. The array version hands the whole block to a C loop that multiplies doubles in a register-friendly pipeline. Forty times faster is typical, and the gap widens for arrays large enough that cache effects matter.

There are real differences beyond speed:

PropertyPython listNumPy array
Element typesAnything, can be mixedOne fixed dtype, all elements same
SizeGrows dynamically with appendFixed at creation, "append" copies
Memory layoutPointers to scattered objectsContiguous block of raw values
Math on the whole thingLoop in PythonSingle C-level operation
Multi-dimensional shapeLists of lists, indexing is awkwardNative, arr[i, j] indexing
Element-wise operatorsOnly + (concatenation) and * (repeat)All arithmetic operators, element-wise

The two are not interchangeable. Lists fit heterogeneous collections, cases where the size is unknown ahead of time, or non-numeric work like joining strings or organizing dictionaries. Arrays fit the moment arithmetic on many numbers in a known shape is involved.

NumPy vs Pandas vs Raw Python

NumPy isn't the only tool in this space. Pandas sits on top of NumPy and adds two things that make tabular data easier: column labels (df["price"] instead of arr[:, 2]) and a row index (which can be a timestamp, a customer ID, anything). Each column of a pandas DataFrame is a NumPy array.

A short guide:

  • Raw Python (lists, dicts): For one-off scripts, mixed-type data, small datasets where readability beats speed, and any code that mostly moves text around.
  • NumPy: For numeric arrays where every element is the same type, the shape is regular, and the work is fast math. Image data, audio samples, model weights, simulations.
  • Pandas: When each column has its own type (a name string, a price float, a placed-at timestamp), for SQL-style filtering and grouping, or when missing values are part of the data. Anything that started life as a CSV usually wants to be a DataFrame.

The boundaries blur in practice. A common workflow: pandas reads a CSV into a DataFrame, the data is filtered and cleaned with pandas, then df.values or df["col"].to_numpy() drops down to the underlying NumPy array for heavy numeric work, and pandas wraps the result back up for display or saving. None of these are mutually exclusive.

For the rest of this section, NumPy handles the math and the array operations, and pandas handles labeled tabular data. The two work together.

A First Real Example

To pull these pieces together, consider a small end-to-end calculation. Daily revenue for one week across three product categories (electronics, books, clothing) is the input. The goal is the total revenue per category and the average per day.

The grid is (7, 3) because there are seven days and three categories. The axis argument to sum and mean controls which dimension to collapse: axis=0 collapses rows (giving one value per column), axis=1 collapses columns (giving one value per row). Calling .sum() with no axis collapses everything to a single grand total.

The aggregations (sum, mean, and axes) are the focus of the operations chapter coming up. The array's shape and dtype, which look like bookkeeping at first, are exactly what makes operations like "sum across days" or "average across categories" a one-liner. With the shape correct, the math falls out.

Quiz

NumPy Basics Quiz

10 quizzes