📊 Section 1 · Foundations 🟢 Beginner MODULE 05

Introduction to NumPy — Arrays & Operations

⏱️ 23 min read
📖 Theory + Code
🧩 5 Quiz Questions
🏗️ 1 Challenge
Your progress in Section 171%
🎯 What you'll learn: Why NumPy exists and how vectorization differs from plain Python loops, how to create arrays with np.array, np.zeros, np.arange and np.linspace, how to inspect an array's shape and dtype, element-wise operations, and basic statistics with np.sum, np.mean and np.std.

Why NumPy?

NumPy ("Numerical Python") provides the ndarray — a fixed-type, fixed-size array — plus a huge library of fast operations that work on entire arrays at once. It's the numerical foundation that Pandas, Matplotlib, and Scikit-Learn are all built on top of.

The problem with plain Python loops

A Python list can hold any mix of types, and every arithmetic operation on it goes through the Python interpreter one element at a time. NumPy arrays fix both of these: every element has the same data type, stored in one contiguous block of memory, and operations run as tight, pre-compiled loops instead of interpreted Python bytecode.

loop_vs_vectorized.py
PYTHON
# The "manual loop" way — pure Python
prices = [19.99, 24.50, 7.25, 42.00]
discounted = []
for p in prices:
    discounted.append(p * 0.9)  # 10% off, one item at a time

# The NumPy "vectorized" way — no explicit loop needed
import numpy as np
prices_arr = np.array(prices)
discounted_arr = prices_arr * 0.9  # applies to every element at once
print(discounted_arr)
"Vectorization" is the key idea
Writing prices_arr * 0.9 instead of a for loop is called vectorization. The loop still happens — but inside NumPy's pre-compiled C code instead of Python's interpreter, which is why vectorized NumPy code is consistently faster than the equivalent hand-written Python loop, especially as arrays grow large.

Creating Arrays

There are several standard ways to build a NumPy array, depending on whether you already have the data or need to generate it.

creating_arrays.py
PYTHON
import numpy as np

# From an existing Python list
a = np.array([10, 20, 30, 40])

# A 2D array from a list of lists
grid = np.array([[1, 2, 3], [4, 5, 6]])

# Arrays of a given shape, filled with zeros or ones
zeros = np.zeros(5)           # [0. 0. 0. 0. 0.]
zeros_2d = np.zeros((2, 3))   # 2 rows, 3 columns, all zeros
ones = np.ones((3, 3))      # 3x3 grid of ones

# A range of values — like Python's range(), but returns an array
r = np.arange(0, 10, 2)     # [0 2 4 6 8] — start, stop (exclusive), step

# N evenly spaced values between two endpoints (inclusive)
lin = np.linspace(0, 1, 5)   # [0.   0.25 0.5  0.75 1.  ]

print(a, grid, zeros_2d, r, lin, sep="\n")
📝
arange vs. linspace
np.arange(start, stop, step) works like Python's range() — you specify the step size, and the count of values is whatever falls out. np.linspace(start, stop, num) works the other way — you specify how many values you want, and NumPy figures out the spacing, including both endpoints.

Array Shape, dtype, and Size

Every array carries metadata describing its dimensions and the type of data it stores. Checking these is one of the first things to do when something isn't behaving as expected.

shape_dtype.py
PYTHON
import numpy as np

grid = np.array([[1, 2, 3], [4, 5, 6]])

print(grid.shape)   # (2, 3) — 2 rows, 3 columns
print(grid.ndim)    # 2 — number of dimensions
print(grid.size)    # 6 — total number of elements
print(grid.dtype)   # int64 (or int32 on some systems) — every element shares one type

# Explicitly choosing a dtype
floats = np.array([1, 2, 3], dtype="float64")
print(floats.dtype)  # float64
print(floats)        # [1. 2. 3.]

# Reshaping — same data, different shape
flat = np.arange(6)             # [0 1 2 3 4 5]
reshaped = flat.reshape(2, 3)  # [[0 1 2] [3 4 5]]
⚠️
Every element shares one dtype
Mixing an integer and a string in np.array([1, 2, "three"]) doesn't raise an error — NumPy silently upcasts everything to a single common type (here, strings), which can quietly break arithmetic later. When something behaves oddly, checking .dtype is often the fastest way to find out why.

Element-Wise Operations

Arithmetic and comparison operators on NumPy arrays apply to every element automatically — no loop required. This is vectorization in action for everyday math.

elementwise_ops.py
PYTHON
import numpy as np

a = np.array([1, 2, 3, 4])
b = np.array([10, 20, 30, 40])

# Array with a single number (a "scalar")
print(a + 5)     # [6 7 8 9]
print(a * 2)     # [2 4 6 8]

# Array with another array of the same shape
print(a + b)     # [11 22 33 44] — element-by-element, NOT concatenation
print(a * b)     # [10 40 90 160]

# Comparisons produce a boolean array
print(a > 2)     # [False False  True  True]
This is different from list behavior
With plain Python lists, [1,2,3] + [10,20,30] concatenates into a 6-item list, and [1,2,3] * 2 repeats the list. NumPy arrays redefine these operators to mean element-wise math instead — a common source of confusion the first time you switch between the two.

Basic Math Functions

NumPy provides fast, built-in functions for the summary statistics you'll compute constantly while exploring a dataset.

basic_stats.py
PYTHON
import numpy as np

scores = np.array([88, 92, 75, 61, 99, 70])

print(np.sum(scores))    # 485 — total
print(np.mean(scores))   # 80.833... — average
print(np.std(scores))    # standard deviation — spread around the mean
print(np.min(scores))    # 61
print(np.max(scores))    # 99
print(np.median(scores)) # 81.5 — middle value when sorted

# These are also available as array methods — both forms are equivalent
print(scores.mean())
print(scores.sum())
📝
Mean vs. standard deviation, in plain terms
The mean tells you the center of the data. The standard deviation tells you how spread out the values are around that center — a small std means most values sit close to the mean; a large std means they're scattered widely. You'll build real statistical intuition for this in Section 4.

Lesson Summary

Let's recap everything you learned in this lesson:

NumPy arrays hold one fixed data type and support vectorized operations — no explicit Python loop needed.
Create arrays with np.array(), np.zeros(), np.ones(), np.arange(), and np.linspace().
Inspect an array with .shape, .ndim, .size, and .dtype.
+, -, *, / on arrays apply element-wise — very different from how these operators behave on Python lists.
np.sum(), np.mean(), np.std(), np.min()/np.max() compute fast summary statistics.
🧩 Knowledge Check — Lesson 5
Answer all 5 questions to test your understanding. Instant feedback on every answer.
1. What does "vectorization" mean in the context of NumPy?
2. What does np.arange(0, 10, 2) produce?
3. For a 2D array with 3 rows and 4 columns, what does .shape return?
4. What does np.array([1, 2, 3]) + np.array([10, 20, 30]) produce?
5. Which function computes the standard deviation of a NumPy array?
💪
Coding Challenge — Lesson 5
Apply what you learned · Beginner Level

Put arrays and vectorized operations to work on a small "dataset."

Challenge: Temperature Report 🌡️

You're given a week of daily high temperatures in Celsius: [22, 25, 19, 30, 28, 24, 21]. Write a script that: (1) creates a NumPy array from this list, (2) converts every value to Fahrenheit using the vectorized formula F = C * 9/5 + 32 (no loop), and (3) prints the mean, minimum, and maximum Fahrenheit temperature, each formatted to 1 decimal place with an f-string.

Rules: No Python for loop for the conversion — use array arithmetic directly. Use np.mean(), np.min(), and np.max() for the statistics.
💡 Show hints if you're stuck
  • Build the array: celsius = np.array([22, 25, 19, 30, 28, 24, 21])
  • Convert all at once: fahrenheit = celsius * 9 / 5 + 32
  • Format with an f-string: f"Mean: {np.mean(fahrenheit):.1f}°F"
Finished this lesson?
Mark it complete to track your progress.
🎉

Lesson 5 Complete!

You can create arrays, inspect them, and vectorize your math. Next up: advanced NumPy — boolean masking, fancy indexing, broadcasting, and a first taste of linear algebra.

Module 05 of 7 Section 1 — Python for Data Science Foundations