DEV Community

Muhammad Ikramullah Khan
Muhammad Ikramullah Khan

Posted on

NumPy for Beginners: The Tricks Nobody Tells You About

You just learned Python. You're comfortable with lists, loops, and basic data structures. Then someone tells you to use NumPy for data work.

You install it. You create an array. It looks like a list. You treat it like a list. Your code runs, but it's slow. Really slow. People keep saying NumPy is fast, but your experience says otherwise.

Here's the problem. NumPy isn't just a faster list. It's a completely different way of thinking. The moment you start using loops with NumPy arrays, you've already lost. The moment you treat NumPy like regular Python, you're missing 90% of its power.

I'm going to show you NumPy the way I wish someone had taught me. Not just the basics (those are everywhere), but the mental shifts and hidden tricks that actually make NumPy worth using.

Let's get started.


What NumPy Actually Is (And Why You Should Care)

NumPy is a library for working with numbers in bulk. Not one number at a time. Bulk.

Think of it like this:

Regular Python is like having a calculator. You can add numbers, but you do it one calculation at a time.

NumPy is like having a spreadsheet. You can apply a formula to an entire column at once. Click once, update 10,000 cells.

The real power:

NumPy operations run in C under the hood. When you write array * 2, NumPy doesn't loop through each element in Python. It drops down to C, multiplies everything in one shot, then gives you back the result.

This is 10-100x faster than Python loops.


Installation (The Right Way)

Don't just pip install numpy. Here's what you actually want:

# Create a virtual environment first
python -m venv data_env
source data_env/bin/activate  # Mac/Linux
# or
data_env\Scripts\activate  # Windows

# Install NumPy
pip install numpy

# Verify it works
python -c "import numpy as np; print(np.__version__)"
Enter fullscreen mode Exit fullscreen mode

You should see something like 1.26.3 or higher.

Pro tip: If you're doing serious data work, install the Anaconda distribution instead. It comes with NumPy and dozens of other scientific libraries pre-configured and optimized.

# Download from anaconda.com, then:
conda install numpy
Enter fullscreen mode Exit fullscreen mode

Anaconda's NumPy is often faster because it's compiled with optimized math libraries (Intel MKL).


Your First Array (The Mistake Everyone Makes)

Here's what most tutorials show you:

import numpy as np

# Create array from a list
my_list = [1, 2, 3, 4, 5]
my_array = np.array(my_list)
print(my_array)  # [1 2 3 4 5]
Enter fullscreen mode Exit fullscreen mode

Cool. You created an array. Now what?

Here's what nobody tells you:

Creating arrays from Python lists is the slowest way to use NumPy. You're defeating the entire purpose. The power of NumPy is in creating large arrays directly, not converting Python objects.

Do this instead:

import numpy as np

# Create arrays directly (no Python lists)
zeros = np.zeros(1000)           # 1000 zeros
ones = np.ones(1000)             # 1000 ones  
range_array = np.arange(0, 100)  # 0 to 99
spaced = np.linspace(0, 1, 50)   # 50 evenly spaced numbers from 0 to 1
Enter fullscreen mode Exit fullscreen mode

These are 10x faster to create than converting lists.


The Golden Rule of NumPy (Memorize This)

If you write a for loop to process a NumPy array, you're doing it wrong.

Let me say that again. No loops.

NumPy is designed for vectorized operations. Operations that work on entire arrays at once.

Bad (slow, defeats the purpose):

import numpy as np

numbers = np.arange(1000000)
result = np.zeros(1000000)

# DON'T DO THIS
for i in range(len(numbers)):
    result[i] = numbers[i] * 2
Enter fullscreen mode Exit fullscreen mode

Good (fast, the NumPy way):

import numpy as np

numbers = np.arange(1000000)
result = numbers * 2  # One line. No loop. 100x faster.
Enter fullscreen mode Exit fullscreen mode

Same result. The second version runs in 0.001 seconds. The first version runs in 0.1 seconds. That's a 100x difference.


The Trick Nobody Teaches: Broadcasting

Broadcasting is NumPy's secret superpower. Once you understand it, everything clicks.

The concept:

NumPy automatically expands smaller arrays to match larger ones when you perform operations.

Example 1: Adding a number to an array

import numpy as np

numbers = np.array([1, 2, 3, 4, 5])
result = numbers + 10

print(result)  # [11 12 13 14 15]
Enter fullscreen mode Exit fullscreen mode

What happened? NumPy took the single number 10 and mentally expanded it to [10, 10, 10, 10, 10], then added element-wise.

You didn't write a loop. NumPy did it for you in C.

Example 2: Arrays of different shapes

import numpy as np

# 2D array (3 rows, 4 columns)
grid = np.array([
    [1, 2, 3, 4],
    [5, 6, 7, 8],
    [9, 10, 11, 12]
])

# 1D array (4 elements)
row = np.array([10, 20, 30, 40])

# Add them together
result = grid + row

print(result)
# [[11 22 33 44]
#  [15 26 37 48]
#  [19 30 41 52]]
Enter fullscreen mode Exit fullscreen mode

NumPy took the row [10, 20, 30, 40] and added it to every row of the grid. No loop needed.

This is broadcasting. NumPy figures out how to align arrays and does the operation element-wise.

The hidden trick:

You can broadcast along any dimension by adding a new axis.

import numpy as np

# 1D array
row = np.array([1, 2, 3])

# Make it a column by adding a dimension
column = row[:, np.newaxis]

print(column)
# [[1]
#  [2]
#  [3]]

# Now you can broadcast it differently
grid = np.array([[10, 20, 30]])
result = column + grid

print(result)
# [[11 21 31]
#  [12 22 32]
#  [13 23 33]]
Enter fullscreen mode Exit fullscreen mode

One number became a column. That column got added to every row of the grid. Still no loops.

This trick alone will save you hundreds of lines of code.


Indexing Secrets

NumPy indexing is way more powerful than Python list indexing. Here are the tricks that actually matter.

Trick 1: Fancy Indexing (Grabbing Multiple Elements)

import numpy as np

data = np.array([10, 20, 30, 40, 50, 60, 70, 80, 90, 100])

# Grab elements at specific positions
indices = [0, 3, 7]
result = data[indices]

print(result)  # [10 40 80]
Enter fullscreen mode Exit fullscreen mode

You can pass a list of indices and NumPy grabs all of them at once. No loop.

Trick 2: Boolean Indexing (Filtering Without Loops)

This is the one that changes everything.

import numpy as np

numbers = np.array([1, 5, 8, 3, 12, 7, 15, 2, 9])

# Get all numbers greater than 7
big_numbers = numbers[numbers > 7]

print(big_numbers)  # [ 8 12 15  9]
Enter fullscreen mode Exit fullscreen mode

What just happened?

numbers > 7 creates a boolean array: [False, False, True, False, True, False, True, False, True]

Then numbers[boolean_array] grabs only the elements where the boolean is True.

No loop. One line.

Combine conditions:

import numpy as np

numbers = np.array([1, 5, 8, 3, 12, 7, 15, 2, 9])

# Numbers between 5 and 12 (inclusive)
middle = numbers[(numbers >= 5) & (numbers <= 12)]

print(middle)  # [ 5  8 12  7  9]
Enter fullscreen mode Exit fullscreen mode

Notice the & (and) operator. Use | for or. Use ~ for not.

Critical gotcha: You MUST use parentheses around each condition. numbers >= 5 & numbers <= 12 will fail. Python's operator precedence will break you.

Trick 3: np.where (Conditional Selection)

import numpy as np

numbers = np.array([1, 5, 8, 3, 12, 7, 15, 2, 9])

# Replace all numbers > 10 with 10, keep others
result = np.where(numbers > 10, 10, numbers)

print(result)  # [ 1  5  8  3 10  7 10  2  9]
Enter fullscreen mode Exit fullscreen mode

np.where(condition, value_if_true, value_if_false) is like a vectorized if-else statement.

Super useful for data cleaning:

import numpy as np

data = np.array([1.5, -999, 3.2, -999, 5.1, 2.8, -999])

# Replace -999 (missing value marker) with 0
cleaned = np.where(data == -999, 0, data)

print(cleaned)  # [1.5 0.  3.2 0.  5.1 2.8 0. ]
Enter fullscreen mode Exit fullscreen mode

Shape Manipulation (The Mind-Bending Part)

Understanding shapes is crucial. This is where beginners get lost.

What Shape Actually Means

import numpy as np

# 1D array (5 elements)
arr1d = np.array([1, 2, 3, 4, 5])
print(arr1d.shape)  # (5,)

# 2D array (3 rows, 4 columns)
arr2d = np.array([
    [1, 2, 3, 4],
    [5, 6, 7, 8],
    [9, 10, 11, 12]
])
print(arr2d.shape)  # (3, 4)

# 3D array (2 blocks, 3 rows, 4 columns)
arr3d = np.array([
    [[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]],
    [[13, 14, 15, 16], [17, 18, 19, 20], [21, 22, 23, 24]]
])
print(arr3d.shape)  # (2, 3, 4)
Enter fullscreen mode Exit fullscreen mode

Shape is a tuple. Each number is a dimension size.

Reshape (Change Shape Without Changing Data)

import numpy as np

# 1D array with 12 elements
data = np.arange(12)
print(data)
# [ 0  1  2  3  4  5  6  7  8  9 10 11]

# Reshape to 3 rows, 4 columns
grid = data.reshape(3, 4)
print(grid)
# [[ 0  1  2  3]
#  [ 4  5  6  7]
#  [ 8  9 10 11]]

# Reshape to 2 blocks, 2 rows, 3 columns
cube = data.reshape(2, 2, 3)
print(cube)
# [[[ 0  1  2]
#   [ 3  4  5]]
#
#  [[ 6  7  8]
#   [ 9 10 11]]]
Enter fullscreen mode Exit fullscreen mode

The rule: Total elements must stay the same. You can't reshape 12 elements into a (5, 5) grid (that would be 25 elements).

The trick: Use -1 to let NumPy calculate one dimension for you.

import numpy as np

data = np.arange(12)

# "Make it 3 rows, figure out the columns yourself"
grid = data.reshape(3, -1)
print(grid.shape)  # (3, 4)

# "Make it 4 columns, figure out the rows yourself"  
grid = data.reshape(-1, 4)
print(grid.shape)  # (3, 4)
Enter fullscreen mode Exit fullscreen mode

NumPy does the math. You avoid mistakes.

Flatten (Back to 1D)

import numpy as np

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

# Back to 1D
flat = grid.flatten()
print(flat)  # [1 2 3 4 5 6]
Enter fullscreen mode Exit fullscreen mode

Or use ravel() which is faster (returns a view instead of a copy).


The axis Parameter (Finally Explained Properly)

This confuses everyone. Let me fix that.

The concept:

When you perform an operation on a multi-dimensional array, axis tells NumPy which dimension to collapse.

Example with a 2D array:

import numpy as np

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

# Sum all elements
total = grid.sum()
print(total)  # 45

# Sum along axis 0 (collapse rows)
column_sums = grid.sum(axis=0)
print(column_sums)  # [12 15 18]

# Sum along axis 1 (collapse columns)
row_sums = grid.sum(axis=1)
print(row_sums)  # [ 6 15 24]
Enter fullscreen mode Exit fullscreen mode

The mental model:

axis=0 means "collapse the rows dimension". You go from (3, 3) to (3,). You get one value per column.

axis=1 means "collapse the columns dimension". You go from (3, 3) to (3,). You get one value per row.

Visual aid:

Original grid (3, 3):
[[1, 2, 3],
 [4, 5, 6],
 [7, 8, 9]]

axis=0 (sum down the rows):
[12, 15, 18]  → shape (3,)

axis=1 (sum across the columns):
[6, 15, 24]  → shape (3,)
Enter fullscreen mode Exit fullscreen mode

The trick nobody tells you:

You can keep dimensions by using keepdims=True.

import numpy as np

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

# Sum columns but keep the 2D shape
result = grid.sum(axis=0, keepdims=True)
print(result)
# [[5 7 9]]

print(result.shape)  # (1, 3) instead of (3,)
Enter fullscreen mode Exit fullscreen mode

Why does this matter? Broadcasting. If you keep dimensions, you can broadcast the result back to the original shape.


Universal Functions (ufuncs) Are Your Best Friends

NumPy has built-in functions that work on entire arrays. No loops needed.

Mathematical Operations

import numpy as np

numbers = np.array([1, 4, 9, 16, 25])

# Square root
sqrt = np.sqrt(numbers)
print(sqrt)  # [1. 2. 3. 4. 5.]

# Exponential
exp = np.exp(numbers)
print(exp)  # Very large numbers

# Logarithm
log = np.log(numbers)
print(log)  # [0.   1.39 2.2  2.77 3.22]

# Trigonometry
angles = np.array([0, np.pi/2, np.pi])
sines = np.sin(angles)
print(sines)  # [0. 1. 0.]
Enter fullscreen mode Exit fullscreen mode

Statistical Operations

import numpy as np

data = np.array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])

print(data.mean())    # 5.5
print(data.std())     # 2.87 (standard deviation)
print(data.min())     # 1
print(data.max())     # 10
print(data.sum())     # 55
print(np.median(data))  # 5.5
Enter fullscreen mode Exit fullscreen mode

The Hidden Gem: Cumulative Operations

import numpy as np

prices = np.array([100, 102, 98, 105, 103])

# Cumulative sum (running total)
running_total = np.cumsum(prices)
print(running_total)  # [100 202 300 405 508]

# Cumulative product
compound = np.cumprod([1.05, 1.05, 1.05, 1.05])
print(compound)  # [1.05   1.1025 1.1576 1.2155]
Enter fullscreen mode Exit fullscreen mode

This is perfect for financial calculations, running totals, or any sequential accumulation.


Memory Tricks (The Stuff That Matters at Scale)

Views vs Copies (This Breaks Production Code)

When you slice a NumPy array, you get a view (reference to original data), not a copy.

import numpy as np

original = np.array([1, 2, 3, 4, 5])
view = original[1:4]

print(view)  # [2 3 4]

# Modify the view
view[0] = 999

# Original changed too!
print(original)  # [  1 999   3   4   5]
Enter fullscreen mode Exit fullscreen mode

This is intentional. Views save memory. But it's dangerous if you don't know it's happening.

When you need a copy:

import numpy as np

original = np.array([1, 2, 3, 4, 5])
copy = original[1:4].copy()

copy[0] = 999

print(original)  # [1 2 3 4 5] (unchanged)
print(copy)      # [999   3   4]
Enter fullscreen mode Exit fullscreen mode

The trick:

Check if something is a view or a copy:

import numpy as np

original = np.array([1, 2, 3, 4, 5])
view = original[1:4]
copy = original[1:4].copy()

print(view.base is original)   # True (it's a view)
print(copy.base is None)        # True (it's a copy)
Enter fullscreen mode Exit fullscreen mode

Memory Layout (Why Some Operations Are Slow)

NumPy arrays can be stored in row-major (C) or column-major (Fortran) order.

import numpy as np

# C-contiguous (rows stored together)
c_array = np.array([[1, 2, 3], [4, 5, 6]], order='C')

# Fortran-contiguous (columns stored together)
f_array = np.array([[1, 2, 3], [4, 5, 6]], order='F')

print(c_array.flags['C_CONTIGUOUS'])  # True
print(f_array.flags['F_CONTIGUOUS'])  # True
Enter fullscreen mode Exit fullscreen mode

Why this matters:

Row operations on C-arrays are faster. Column operations on F-arrays are faster.

Most of the time, stick with C (default). But if you're doing heavy column operations (common in some scientific computing), F-order can be 10x faster.


Real-World Example: Data Analysis Without Pandas

Let's process some actual data using only NumPy.

Scenario: You have sales data. Each row is a transaction: [product_id, quantity, price_per_unit].

import numpy as np

# Sales data: [product_id, quantity, price_per_unit]
sales = np.array([
    [101, 5, 10.50],
    [102, 3, 25.00],
    [101, 2, 10.50],
    [103, 1, 150.00],
    [102, 4, 25.00],
    [101, 3, 10.50],
    [103, 2, 150.00],
])

# Calculate total revenue per transaction
revenue_per_transaction = sales[:, 1] * sales[:, 2]
print("Revenue per transaction:", revenue_per_transaction)
# [ 52.5   75.    21.   150.   100.    31.5  300.  ]

# Total revenue
total_revenue = revenue_per_transaction.sum()
print("Total revenue: $", total_revenue)
# Total revenue: $ 730.0

# Average transaction value
avg_transaction = revenue_per_transaction.mean()
print("Average transaction: $", avg_transaction)
# Average transaction: $ 104.29

# Find product 101 transactions
product_101_mask = sales[:, 0] == 101
product_101_sales = sales[product_101_mask]
print("Product 101 sales:")
print(product_101_sales)
# [[101.    5.   10.5 ]
#  [101.    2.   10.5 ]
#  [101.    3.   10.5 ]]

# Total units sold for product 101
product_101_units = product_101_sales[:, 1].sum()
print("Product 101 units sold:", product_101_units)
# Product 101 units sold: 10.0

# Revenue for product 101
product_101_revenue = (product_101_sales[:, 1] * product_101_sales[:, 2]).sum()
print("Product 101 revenue: $", product_101_revenue)
# Product 101 revenue: $ 105.0
Enter fullscreen mode Exit fullscreen mode

No pandas. No loops. Just NumPy. Fast and efficient.


Performance Tips (Make Your Code 100x Faster)

Tip 1: Pre-allocate Arrays

import numpy as np

# Bad (slow, reallocates every iteration)
result = np.array([])
for i in range(10000):
    result = np.append(result, i * 2)

# Good (fast, pre-allocated)
result = np.zeros(10000)
for i in range(10000):
    result[i] = i * 2

# Best (no loop at all)
result = np.arange(10000) * 2
Enter fullscreen mode Exit fullscreen mode

The first version is 100x slower. The third version is 1000x faster than the first.

Tip 2: Use In-Place Operations

import numpy as np

data = np.arange(1000000)

# Creates a new array
data = data * 2

# Modifies in place (saves memory)
data *= 2
Enter fullscreen mode Exit fullscreen mode

In-place operations (+=, -=, *=, /=) modify the array without creating a copy.

Tip 3: Avoid Python Loops at All Costs

import numpy as np

data = np.random.rand(1000000)

# Terrible (10 seconds)
result = []
for x in data:
    result.append(x ** 2)
result = np.array(result)

# Good (0.01 seconds)
result = data ** 2
Enter fullscreen mode Exit fullscreen mode

1000x speed difference. Always vectorize.


Common Mistakes (And How to Fix Them)

Mistake 1: Comparing Arrays with == Directly

import numpy as np

a = np.array([1, 2, 3])
b = np.array([1, 2, 3])

# This doesn't do what you think
if a == b:  # Returns an array, not a boolean!
    print("Equal")
Enter fullscreen mode Exit fullscreen mode

This gives a warning because a == b returns [True, True, True], not a single boolean.

Fix:

import numpy as np

a = np.array([1, 2, 3])
b = np.array([1, 2, 3])

# Check if all elements are equal
if np.array_equal(a, b):
    print("Equal")

# Or use .all()
if (a == b).all():
    print("Equal")
Enter fullscreen mode Exit fullscreen mode

Mistake 2: Modifying Arrays During Iteration

import numpy as np

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

# Don't do this
for i, val in enumerate(data):
    if val > 2:
        data[i] = 0  # Dangerous if you're iterating

# Do this instead (vectorized)
data[data > 2] = 0
Enter fullscreen mode Exit fullscreen mode

Mistake 3: Not Checking Shapes

import numpy as np

a = np.array([1, 2, 3])
b = np.array([[1], [2], [3]])

# This will fail or give unexpected results
# result = a + b

# Always check shapes first
print(a.shape)  # (3,)
print(b.shape)  # (3, 1)

# Reshape if needed
b_flat = b.flatten()
result = a + b_flat
Enter fullscreen mode Exit fullscreen mode

Quick Reference

Create arrays:

np.zeros(10)              # 10 zeros
np.ones(10)               # 10 ones
np.arange(0, 100, 5)      # 0, 5, 10, ..., 95
np.linspace(0, 1, 50)     # 50 evenly spaced from 0 to 1
np.random.rand(10)        # 10 random numbers [0, 1)
Enter fullscreen mode Exit fullscreen mode

Shape operations:

arr.reshape(3, 4)         # Change shape
arr.flatten()             # Make 1D
arr.T                     # Transpose
Enter fullscreen mode Exit fullscreen mode

Indexing:

arr[5]                    # Single element
arr[2:5]                  # Slice
arr[[1, 3, 5]]            # Fancy indexing
arr[arr > 10]             # Boolean indexing
Enter fullscreen mode Exit fullscreen mode

Math:

arr + 5                   # Add to all
arr * 2                   # Multiply all
arr ** 2                  # Square all
np.sqrt(arr)              # Square root
np.sum(arr)               # Sum all
arr.mean()                # Average
Enter fullscreen mode Exit fullscreen mode

Conditions:

arr[arr > 5]              # Filter
np.where(arr > 5, 1, 0)   # Conditional replace
Enter fullscreen mode Exit fullscreen mode

Summary

NumPy is not just a faster list. It's a different paradigm. The moment you embrace vectorization and stop writing loops, everything gets faster.

Key mental shifts:

  • No loops. Ever. Use vectorized operations.
  • Broadcasting is your superpower. Learn it.
  • Boolean indexing replaces most filter loops.
  • Pre-allocate arrays. Don't grow them dynamically.
  • Understand views vs copies or your code will break in production.

The tricks that actually matter:

  • Use np.where() instead of if-else loops
  • Use keepdims=True for broadcasting
  • Check .base to see if you have a view or copy
  • Use -1 in reshape to let NumPy calculate dimensions
  • Always check shapes before operations

Start with these:

  1. Create arrays directly (not from lists)
  2. Use boolean indexing to filter data
  3. Use broadcasting to avoid loops
  4. Use axis parameter to collapse dimensions
  5. Pre-allocate arrays when you must loop

Once you internalize these, NumPy becomes 10x more powerful.

Happy computing!


Resources:

Top comments (0)