This NumPy introduction gets you started with the library from scratch. You will see why NumPy arrays beat plain Python lists by a wide margin, learn how to create an ndarray, understand dtypes and shape, and run your first vectorized operations.
“NumPy is the foundation of the Python scientific computing stack.”
Travis Oliphant, NumPy creator
Last Updated: July 2026 | Tested on: Python 3.14.6, NumPy 2.4.6 | Difficulty: Intermediate | Reading Time: 20 minutes
You have been writing Python loops to add numbers, work out averages, and transform lists. It works. It also gets painfully slow once your dataset grows to a million rows. NumPy exists because someone got tired of waiting for Python loops to finish. Instead of chewing through one number at a time, NumPy hands the whole array to compiled C code that processes everything in one shot. The result: work that drags on for seconds with Python lists wraps up in milliseconds with NumPy.
Think of it like making tea for a full office. The Python-loop way is boiling one cup at a time, twenty times over. The NumPy way is one big kettle, all twenty cups at once. Same tea, a fraction of the wait. That single idea, doing the whole batch together instead of one item at a time, is the heart of everything in this post.
It also matters because every data science library you will meet in this part (Pandas, Matplotlib, scikit-learn, TensorFlow, PyTorch) is built on top of NumPy, which makes a numpy introduction the real first step into all of them. Learning NumPy arrays is not optional. It is the difference between writing data science code that happens to run and writing code you actually understand.
A developer friend of mine, Pravin, once tried to crunch a multi-million-row dataset using Python lists and a for loop. He watched the progress bar crawl, then rewrote the hot part with NumPy. Same result, but tens of times faster, the kind of speedup you feel the moment you hit Enter. On the benchmark below it lands around 35x on my laptop, and your exact number will depend on your machine.
Tip: click the diagram to open it full screen, then use the zoom and pan controls for a closer look.
The diagram contrasts NumPy’s ndarray memory layout with a Python list. An ndarray stores its values in one continuous block of typed memory, side by side. A Python list instead stores pointers to separate objects scattered around the heap. That contiguous layout is the whole reason NumPy operations run so much faster. The Central Processing Unit (CPU) can pull array chunks straight into its cache and crunch them with SIMD (Single Instruction, Multiple Data) instructions, one instruction working on several numbers at once, instead of hopping from pointer to pointer. Almost every speed advantage NumPy gives you traces back to this one memory layout difference.
Table of Contents
Prerequisites
Complete math for data science tutorial. Familiarity with lists tutorial and list comprehensions tutorial is expected. Beyond that, this numpy introduction assumes no prior NumPy experience.
Install & Verify
📄 Terminal: Install NumPy
pip install numpy
📄 verify_numpy.py: Check your installation
import numpy as np
print(f"NumPy version: {np.__version__}")
print(f"Quick test: {np.array([1, 2, 3]) + 10}")
▶ Output
NumPy version: 2.4.6 Quick test: [11 12 13]
If you see the version and the array output, you are ready for the rest of this numpy introduction. Your version number may read higher than mine if you install later; this post was tested on NumPy 2.4.6. The convention import numpy as np is universal. Every tutorial, every codebase, every StackOverflow answer uses np, so do not fight it.
Why Every NumPy Introduction Starts with Speed
📄 speed_comparison.py: Python list vs NumPy array
import numpy as np
import time
size = 1_000_000
# Python list approach
python_list = list(range(size))
start = time.perf_counter()
result_list = [x * 2 for x in python_list]
python_time = time.perf_counter() - start
# NumPy array approach
numpy_array = np.arange(size)
start = time.perf_counter()
result_numpy = numpy_array * 2
numpy_time = time.perf_counter() - start
print(f"Python list: {python_time:.4f} seconds")
print(f"NumPy array: {numpy_time:.4f} seconds")
print(f"NumPy is {python_time / numpy_time:.0f}x faster!")
print(f"\nFirst 5 results match: {result_list[:5]} == {result_numpy[:5].tolist()}")
▶ Output
Python list: 0.1038 seconds NumPy array: 0.0030 seconds NumPy is 35x faster! First 5 results match: [0, 2, 4, 6, 8] == [0, 2, 4, 6, 8]
What happened here: Same operation (multiply every element by 2), same result, but NumPy finished it about 35x faster on this run. Timings bounce around a bit between runs and machines, so do not chase the exact number; the gap of tens of times is the point. Here is why it happens. A Python list stores pointers to separate Python objects scattered across memory, and the loop has to visit each one. NumPy stores the raw numbers in one contiguous block, so when you write numpy_array * 2 it hands that whole block to a compiled C function that does the math without ever building a single Python object. That batch-it-all-at-once trick is what people mean by vectorization.
Creating Arrays: Your First ndarrays
Think of creating arrays like ordering notebooks from a print shop: np.array() copies your handwritten page (a Python list) into a fresh notebook, np.zeros() delivers blank pages, np.full() arrives pre-printed with the same number on every line, and the random generator scribbles in values for you when all you need is test data. Here are the constructors you will actually reach for day to day.
📄 creating_arrays.py: Multiple ways to create NumPy arrays
import numpy as np
# From a Python list
scores = np.array([85, 92, 78, 95, 88])
print(f"From list: {scores}")
print(f"Type: {type(scores)}")
print(f"dtype: {scores.dtype}")
print(f"Shape: {scores.shape}")
# 2D array (matrix) from nested lists
matrix = np.array([[1, 2, 3],
[4, 5, 6]])
print(f"\n2D array:\n{matrix}")
print(f"Shape: {matrix.shape}") # (rows, columns)
# Common constructors
print(f"\nzeros: {np.zeros(5)}")
print(f"ones: {np.ones(3)}")
print(f"full: {np.full(4, 7)}") # Fill with 7
print(f"arange: {np.arange(0, 10, 2)}") # Start, stop, step
print(f"linspace: {np.linspace(0, 1, 5)}") # 5 evenly spaced from 0 to 1
# Random arrays
rng = np.random.default_rng(42)
print(f"\nRandom ints: {rng.integers(1, 100, size=5)}")
print(f"Random floats: {rng.random(5).round(3)}")
print(f"Random normal: {rng.normal(loc=0, scale=1, size=5).round(3)}")
▶ Output
From list: [85 92 78 95 88] Type:dtype: int64 Shape: (5,) 2D array: [[1 2 3] [4 5 6]] Shape: (2, 3) zeros: [0. 0. 0. 0. 0.] ones: [1. 1. 1.] full: [7 7 7 7] arange: [0 2 4 6 8] linspace: [0. 0.25 0.5 0.75 1. ] Random ints: [ 9 77 65 44 43] Random floats: [0.697 0.094 0.976 0.761 0.786] Random normal: [-0.017 -0.853 0.879 0.778 0.066]
What happened here: Every NumPy array carries two key labels, a dtype (data type) and a shape. The dtype tells NumPy how to read the raw bytes; int64 means each element is a 64-bit integer. The shape is a tuple that describes the dimensions: (5,) is a 1D array with 5 elements, and (2, 3) is a 2D array with 2 rows and 3 columns. One thing to note about the random lines: np.random.default_rng() is the modern way to make a random generator, and it replaces the older np.random.seed() style. Because we seeded it with 42, you get these exact same numbers every time you run the script, which is what you want when results need to be reproducible.
Data Types: Why dtype Matters
Unlike a Python list, which can hold anything ([1, "hello", 3.14, None]), every element in a NumPy array has to be the same type. That rule sounds limiting, but it is exactly what makes NumPy fast. Picture a row of identical lockers, each one the same size. Because every locker is the same, you can jump straight to locker number 500 without measuring the ones before it. NumPy works the same way: when it knows every element is a 64-bit float, it can compute the memory address of any element instantly and lean on hardware-optimized math instructions.
📄 dtypes.py: Understanding and controlling data types
import numpy as np
# NumPy infers the dtype
integers = np.array([1, 2, 3])
floats = np.array([1.0, 2.0, 3.0])
mixed = np.array([1, 2.5, 3]) # Upcasts to float!
print(f"Integers dtype: {integers.dtype}")
print(f"Floats dtype: {floats.dtype}")
print(f"Mixed dtype: {mixed.dtype}") # float64, not mixed!
# Specify dtype explicitly
small_ints = np.array([1, 2, 3], dtype=np.int8) # -128 to 127
big_ints = np.array([1, 2, 3], dtype=np.int64) # Huge range
half_floats = np.array([1.0, 2.0], dtype=np.float16) # ML uses this
print(f"\nint8 uses: {small_ints.nbytes} bytes for 3 elements")
print(f"int64 uses: {big_ints.nbytes} bytes for 3 elements")
print(f"float16 uses: {half_floats.nbytes} bytes for 2 elements")
# Type casting (note: truncates toward zero, does NOT round)
prices = np.array([19.99, 25.50, 7.25])
truncated = prices.astype(np.int32)
print(f"\nPrices: {prices} -> Truncated: {truncated}")
# Boolean arrays (crucial for filtering)
ages = np.array([22, 34, 19, 28, 31])
adults = ages >= 21
print(f"\nAges: {ages}")
print(f"Adults (>= 21): {adults}")
print(f"Adult ages: {ages[adults]}")
▶ Output
Integers dtype: int64 Floats dtype: float64 Mixed dtype: float64 int8 uses: 3 bytes for 3 elements int64 uses: 24 bytes for 3 elements float16 uses: 4 bytes for 2 elements Prices: [19.99 25.5 7.25] -> Truncated: [19 25 7] Ages: [22 34 19 28 31] Adults (>= 21): [ True True False True True] Adult ages: [22 34 28 31]
What happened here: When you mix integers and floats in one array, NumPy upcasts everything to float64 so no value gets silently rounded away. Picking a smaller dtype (int8 instead of int64) saves memory, which really starts to matter once you load millions of rows. Also watch the casting line: astype(np.int32) simply chops off the decimal part, so 19.99 becomes 19, not 20. If you want real rounding, call np.round() first, then cast. The last block shows boolean indexing: ages >= 21 builds a True/False mask, and ages[adults] keeps only the elements where the mask is True. That is exactly how you filter data in NumPy and Pandas, and you will reach for this pattern constantly.
Shape & Reshape: Thinking in Dimensions
An array’s shape is simply how NumPy chooses to lay out the same underlying numbers: a flat line, a grid, or a stack of grids. Reshaping rearranges that layout without touching the values themselves. Let us see it in action.
📄 shape_reshape.py: Reshaping arrays without copying data
import numpy as np
# 1D array with 12 elements
data = np.arange(1, 13)
print(f"1D: {data} shape: {data.shape}")
# Reshape to 2D: 3 rows x 4 columns
matrix = data.reshape(3, 4)
print(f"\n3x4 matrix:\n{matrix}")
print(f"Shape: {matrix.shape}")
# Reshape to 2D: 4 rows x 3 columns
matrix2 = data.reshape(4, 3)
print(f"\n4x3 matrix:\n{matrix2}")
# Use -1 to auto-calculate one dimension
auto = data.reshape(2, -1) # 2 rows, NumPy figures out 6 cols
print(f"\nAuto-shaped (2, -1):\n{auto}")
print(f"Shape: {auto.shape}")
# 3D array: think of it as "layers of matrices"
cube = data.reshape(2, 2, 3)
print(f"\n3D array (2 layers, 2 rows, 3 cols):\n{cube}")
print(f"Shape: {cube.shape}")
# Flatten back to 1D
flat = cube.ravel()
print(f"\nFlattened: {flat}")
▶ Output
1D: [ 1 2 3 4 5 6 7 8 9 10 11 12] shape: (12,) 3x4 matrix: [[ 1 2 3 4] [ 5 6 7 8] [ 9 10 11 12]] Shape: (3, 4) 4x3 matrix: [[ 1 2 3] [ 4 5 6] [ 7 8 9] [10 11 12]] Auto-shaped (2, -1): [[ 1 2 3 4 5 6] [ 7 8 9 10 11 12]] Shape: (2, 6) 3D array (2 layers, 2 rows, 3 cols): [[[ 1 2 3] [ 4 5 6]] [[ 7 8 9] [10 11 12]]] Shape: (2, 2, 3) Flattened: [ 1 2 3 4 5 6 7 8 9 10 11 12]
What happened here: reshape() only changes how NumPy reads the same block of memory. No data gets copied; just the metadata (the shape and the strides) changes. Think of the 12 numbers as a single strip of stickers: reshaping is choosing whether to lay them out as 3 rows of 4 or 4 rows of 3, without ever reprinting the stickers. The -1 trick is the handy part. When you write reshape(2, -1), NumPy works out the missing dimension for you (12 divided by 2 is 6). In machine learning you will reshape arrays constantly to match the shape a model expects, so this becomes muscle memory fast.
Vectorized Operations: No Loops Needed
Vectorized code is like pinning one notice on the society noticeboard instead of knocking on every flat’s door: you act once, and every element gets the message at the same time. Write a + b and NumPy applies the addition to every pair of elements for you, no loop in sight.
📄 vectorized.py: Element-wise operations without loops
import numpy as np
# Arithmetic operations work element-wise
a = np.array([10, 20, 30, 40])
b = np.array([1, 2, 3, 4])
print(f"a + b = {a + b}")
print(f"a - b = {a - b}")
print(f"a * b = {a * b}") # Element-wise, NOT matrix multiplication
print(f"a / b = {a / b}")
print(f"a ** 2 = {a ** 2}")
print(f"a % 3 = {a % 3}")
# Comparison operators return boolean arrays
print(f"\na > 20: {a > 20}")
print(f"a == 30: {a == 30}")
# Math functions (ufuncs, short for universal functions)
angles = np.array([0, 30, 45, 60, 90])
radians = np.radians(angles)
print(f"\nsin({angles}) = {np.sin(radians).round(3)}")
print(f"sqrt({a}) = {np.sqrt(a).round(3)}")
print(f"log({a}) = {np.log(a).round(3)}")
# Aggregation functions
scores = np.array([85, 92, 78, 95, 88, 74, 91])
print(f"\nScores: {scores}")
print(f"Sum: {scores.sum()}")
print(f"Mean: {scores.mean():.1f}")
print(f"Std: {scores.std():.1f}")
print(f"Min: {scores.min()}")
print(f"Max: {scores.max()}")
print(f"Argmax: {scores.argmax()} (index of max)")
▶ Output
a + b = [11 22 33 44] a - b = [ 9 18 27 36] a * b = [ 10 40 90 160] a / b = [10. 10. 10. 10.] a ** 2 = [ 100 400 900 1600] a % 3 = [1 2 0 1] a > 20: [False False True True] a == 30: [False False True False] sin([ 0 30 45 60 90]) = [0. 0.5 0.707 0.866 1. ] sqrt([10 20 30 40]) = [3.162 4.472 5.477 6.325] log([10 20 30 40]) = [2.303 2.996 3.401 3.689] Scores: [85 92 78 95 88 74 91] Sum: 603 Mean: 86.1 Std: 7.1 Min: 74 Max: 95 Argmax: 3 (index of max)
What happened here: Each operation runs on every element at once. No for loops, no list comprehensions, you just write the math you mean. One thing to remember: the * operator does element-wise multiplication, not matrix multiplication, so reach for @ or np.matmul() when you actually want a matrix product. Aggregation methods like .sum() and .mean() squeeze the whole array down to a single number. And argmax() hands back the index of the largest value, which turns out to be how you read off a model’s prediction in machine learning (the position with the highest score wins).
Practical Workflow: Student Grade Analysis
Enough isolated examples. Here is where the pieces of this numpy introduction come together, on a small task close to something you would actually do: a class of 5 students named Rahul, Niranjan, Viraj, Aviraj, and Aditi, each with marks in 4 subjects. You want per-student averages, per-subject averages, the topper, and grades rescaled to a tidy 0 to 1 range. Notice the whole thing has zero for loops over the numbers themselves; the only loops are for printing. NumPy does the math in one move.
📄 grade_analysis.py: A realistic NumPy workflow
import numpy as np
# Simulating grade data: 5 students, 4 subjects
# Rows = students, Columns = subjects (Math, Science, English, History)
rng = np.random.default_rng(42)
students = ["Rahul", "Niranjan", "Viraj", "Aviraj", "Aditi"]
subjects = ["Math", "Science", "English", "History"]
grades = rng.integers(55, 100, size=(5, 4))
print("Grade Matrix:")
print(f"{'':>12} {subjects[0]:>8} {subjects[1]:>8} {subjects[2]:>8} {subjects[3]:>8}")
for i, name in enumerate(students):
print(f"{name:>12} {grades[i][0]:>8} {grades[i][1]:>8} {grades[i][2]:>8} {grades[i][3]:>8}")
# Per-student average (mean across columns, axis=1)
student_avg = grades.mean(axis=1)
print(f"\nStudent Averages:")
for name, avg in zip(students, student_avg):
print(f" {name}: {avg:.1f}")
# Per-subject average (mean across rows, axis=0)
subject_avg = grades.mean(axis=0)
print(f"\nSubject Averages:")
for subj, avg in zip(subjects, subject_avg):
print(f" {subj}: {avg:.1f}")
# Who scored highest overall?
best_idx = student_avg.argmax()
print(f"\nTop student: {students[best_idx]} ({student_avg[best_idx]:.1f})")
# Normalize grades to 0-1 range (min-max scaling)
normalized = (grades - grades.min()) / (grades.max() - grades.min())
print(f"\nNormalized (first student): {normalized[0].round(3)}")
▶ Output
Grade Matrix:
Math Science English History
Rahul 59 89 84 74
Niranjan 74 93 58 86
Viraj 64 59 78 98
Aviraj 88 89 87 90
Aditi 78 60 92 75
Student Averages:
Rahul: 76.5
Niranjan: 77.8
Viraj: 74.8
Aviraj: 88.5
Aditi: 76.2
Subject Averages:
Math: 72.6
Science: 78.0
English: 79.8
History: 84.6
Top student: Aviraj (88.5)
Normalized (first student): [0.025 0.775 0.65 0.4 ]
What happened here: The axis parameter is the part that trips people up, so hold onto this rule: the axis you name is the one that disappears. axis=1 collapses the columns, leaving one average per student (one number per row). axis=0 collapses the rows, leaving one average per subject (one number per column). On these seeded grades Aviraj comes out on top with an 88.5 average. The last line does min-max normalization, which rescales every value into the 0 to 1 range, and you will use this exact pattern when you scale features for machine learning. Since the generator is seeded with 42, your numbers will match these exactly.
Ecosystem: What NumPy Connects To
NumPy is not an island, and no numpy introduction is complete without a look at what builds on top of it:
- Pandas (Pandas introduction): DataFrames store data as NumPy arrays underneath
- Matplotlib (Matplotlib basics tutorial): Plotting functions accept NumPy arrays directly
- scikit-learn (ML setup tutorial): ML (machine learning) models expect NumPy arrays as input
- SciPy: Advanced math, optimization, and signal processing, all built on NumPy
- PyTorch/TensorFlow (the PyTorch through TensorFlow and Keras tutorials): Tensors behave like GPU (Graphics Processing Unit) accelerated NumPy arrays, with a near-identical API
Common Mistakes
📄 Mistake 1: Using Python loops instead of vectorized operations
import numpy as np arr = np.arange(1000000) # BAD: defeats the purpose of NumPy result_bad = np.array([x * 2 + 1 for x in arr]) # GOOD: vectorized, tens of times faster result_good = arr * 2 + 1
📄 Mistake 2: Confusing * (element-wise) with @ (matrix multiplication)
import numpy as np a = np.array([[1, 2], [3, 4]]) b = np.array([[5, 6], [7, 8]]) print(a * b) # [[5, 12], [21, 32]] element-wise! print(a @ b) # [[19, 22], [43, 50]] matrix multiplication
📄 Mistake 3: Mutating a view thinking it is a copy
import numpy as np original = np.array([1, 2, 3, 4, 5]) view = original[1:4] # This is a VIEW, not a copy! view[0] = 999 print(original) # [ 1 999 3 4 5] original changed! # Use .copy() to avoid this safe_copy = original[1:4].copy() safe_copy[0] = 0 print(original) # [ 1 999 3 4 5] original unchanged
Practice Exercises
- Exercise 1: Create a 1D array of the numbers 1 to 20 with
np.arange, then reshape it into a 4 by 5 matrix. Print its shape, then use a boolean mask to pull out only the even numbers. - Exercise 2: Take the grade matrix from the workflow section. Find each student’s highest single subject score with
grades.max(axis=1), and useargmax(axis=1)to find which subject that was. Print the subject name, not just the index. - Exercise 3: Rerun the speed benchmark, but change the operation from
* 2tonp.sqrt(...). Does NumPy still win by the same margin? Try sizes of 10,000, then 1,000,000, then 10,000,000 and watch how the gap grows.
Conclusion
This numpy introduction showed you why NumPy exists and how its core pieces fit together: an ndarray keeps its values in one contiguous block of typed memory, dtype and shape describe that block, and vectorized operations hand the whole batch to compiled C code while you write plain math. You also picked up two habits that carry through all of data science: boolean masks for filtering and the axis rule (“the axis you name is the one that disappears”) for aggregations. Next, we go deeper into array creation with zeros, ones, arange, and linspace. To browse every post in order, visit the Python + AI/ML tutorial series home.
Frequently Asked Questions
What is the difference between a Python list and a NumPy array?
A Python list can hold mixed types and stores pointers to scattered objects. A NumPy array holds one data type in contiguous memory. NumPy is 10-100x faster for numerical operations because it uses optimized C code and CPU cache-friendly memory layout.
When should I use NumPy vs Pandas?
Use NumPy for pure numerical computation (matrix math, signal processing, image processing). Use Pandas when your data has labeled columns and mixed types (like a CSV file). Pandas is built on NumPy, so they work together without friction.
What does axis=0 and axis=1 mean?
axis=0 operates along rows (collapses rows, gives one result per column). axis=1 operates along columns (collapses columns, gives one result per row). Think of it as: the axis number is the dimension that disappears after the operation.
What is the difference between np.array and np.ndarray?
np.array() is a function that creates arrays. np.ndarray is the class. Always use np.array() to create arrays. Direct np.ndarray() construction is low-level and rarely needed.
Does NumPy work with GPU?
Not directly. NumPy runs on CPU only. For GPU-accelerated arrays, use CuPy (drop-in NumPy replacement for NVIDIA GPUs) or PyTorch/TensorFlow tensors. CuPy uses the same API as NumPy, so switching is often just changing the import.
Why does NumPy use import numpy as np convention?
Convention established by the NumPy community. Since NumPy functions are called hundreds of times in a typical script, np.array() is much more readable than numpy.array(). Every numpy introduction, documentation page, and StackOverflow answer uses np, so it is worth adopting from your first script.
Interview Questions on NumPy
How interviewers actually probe this topic: real scenarios, with answers you can say out loud.
Q: What is vectorization, and why is it faster than a Python for loop?
Vectorization means applying an operation to a whole array at once instead of element by element. When you write arr * 2, NumPy hands the entire contiguous memory block to a compiled C routine that never builds intermediate Python objects. Because the data sits side by side in memory, the CPU can stream it through cache and use SIMD instructions that process several numbers per instruction. A Python loop, by contrast, pays interpreter overhead and a pointer chase for every single element, which is why the gap grows to tens of times on large arrays.
Q: What is the difference between a view and a copy in NumPy?
A view shares the same underlying data buffer as the original array, so writing to the view mutates the original. Basic slicing like arr[1:4] returns a view, not a copy. To get an independent array you must call .copy() explicitly. Fancy indexing (boolean masks or integer index arrays) returns a copy instead, which is a common source of confusion.
Q: Scenario: you slice a large array, modify the slice, and later notice the original array changed unexpectedly. What happened, and how do you prevent it?
The slice was a view, not a copy, so your writes landed in the shared buffer and the original saw them. This is expected NumPy behaviour, not a bug. If you need the slice to be independent, create it with arr[1:4].copy(). As a debugging check, slice.base is not None when the array is a view of another buffer, which quickly confirms whether you are looking at a view.
Q: Scenario: a NumPy computation over a million rows is unexpectedly slow and memory spikes. What do you check first?
First check that you are not looping in Python or wrapping a list comprehension around the array, since that defeats vectorization entirely; replace it with a whole-array expression. Next check the dtype: an object dtype (often caused by mixed types or Python ints) stores pointers rather than raw numbers and is both slow and memory-hungry, so cast to a concrete numeric dtype like float64 or a smaller one. Finally, watch for accidental copies from chained operations and reuse buffers or the out= parameter where it matters.
Q: What does the -1 mean in reshape, and when is it useful?
The -1 tells NumPy to infer that one dimension from the total number of elements and the other dimensions you specified. For example, on a 12-element array reshape(2, -1) produces a (2, 6) array because NumPy solves 12 / 2 = 6. It is handy when you know one dimension but do not want to hand-calculate the other, such as reshape(-1, 1) to turn a flat (n,) array into the (n, 1) column shape many machine learning models expect.
Q: Why does astype(np.int32) on [19.99, 25.50, 7.25] give [19, 25, 7] instead of rounding?
Casting a float array to an integer dtype truncates toward zero, it does not round to nearest. So 19.99 drops its decimal part and becomes 19, and 25.50 becomes 25. If you want proper rounding, call np.round() first and then cast, for example np.round(prices).astype(np.int32). Forgetting this is a classic off-by-one bug when converting scores or currency.
Q: What is the difference between the * and @ operators on NumPy arrays?
The * operator does element-wise multiplication: it multiplies matching positions and requires broadcast-compatible shapes. The @ operator (equivalent to np.matmul()) does true matrix multiplication, where the inner dimensions must line up. For two 2×2 matrices, * multiplies cell by cell while @ computes row-by-column dot products, so mixing them up silently produces wrong numbers rather than an error.
Further reading: NumPy official documentation is the authoritative source on this.
Related Posts
Previous: Python: Math for Data Science, The Only Math You Need
Next: NumPy: Array Creation with zeros, ones, arange, linspace
Series Home: Python + AI/ML Tutorial Series

No comment