NumPy Guide — Python's Foundation for Data Science

Photo: Christina Morillo / Pexels
NumPy is the layer almost everything else in Python's numeric stack sits on. pandas DataFrames store their columns as NumPy arrays under the hood, scikit-learn's fit methods expect NumPy arrays as input, and PyTorch tensors were deliberately designed to mirror NumPy's API. Nature's 2020 retrospective on the project — co-authored by 26 of its core maintainers — describes NumPy as the array-computing substrate for "virtually all of Python data science and computational science." Understanding it changes how you write numeric code: loops disappear, code gets shorter, and typical execution time drops by one to two orders of magnitude.
What NumPy actually is, and why it's fast
At its core, NumPy provides one data structure: the ndarray, an N-dimensional array of fixed-type elements stored in a single, contiguous block of memory. That single design decision — contiguous, fixed-type storage instead of Python's default object model — is the source of almost every performance and ergonomics gain NumPy delivers.
A plain Python list like [1, 2, 3] is not actually a row of integers sitting in memory. It's a row of pointers, each one pointing to a separate int object scattered somewhere on the heap. Every one of those int objects carries Python's normal object overhead: a reference count, a type pointer, and the value itself, which on CPython adds up to roughly 28 bytes per integer even though the number itself needs far fewer. Adding two lists element-wise means Python's interpreter re-enters its bytecode dispatch loop on every single element, checking types and dereferencing pointers each time.
A NumPy array skips all of that. It stores raw bytes packed edge to edge — 8 bytes per int64, no padding, no per-element object headers — and hands off the actual math to pre-compiled C and Fortran routines that use CPU vector instructions (SIMD) to process multiple values per clock cycle. The result, according to NumPy's own documentation and echoed across the numeric-Python literature, is that vectorized array operations typically run 50 to 200 times faster than the equivalent pure-Python loop for numeric workloads (NumPy documentation, numpy.org/doc/stable).
import numpy as np
import time
py_list = list(range(1_000_000))
np_arr = np.arange(1_000_000)
t0 = time.time(); sum(x * x for x in py_list); print(f"Python: {time.time()-t0:.3f}s")
t0 = time.time(); (np_arr ** 2).sum(); print(f"NumPy: {time.time()-t0:.3f}s")
# Python: ~0.18s NumPy: ~0.002s (~90x speedup on this machine)
Think of the difference like shipping. A Python list is like mailing 1,000 individually wrapped, individually addressed packages — the postal system has to look up and process each one separately. A NumPy array is like loading 1,000 identical items into one shipping container with a single manifest. The container moves as one unit; there's no per-item overhead once it's loaded.
Creating arrays
NumPy gives you several constructors depending on whether you're wrapping existing data, generating a pattern, or filling with random values.
import numpy as np
np.array([1, 2, 3]) # from a list
np.array([[1, 2], [3, 4]]) # 2D from nested list
np.zeros((3, 4)) # 3x4 of zeros
np.ones((2, 2), dtype=np.int32) # specify dtype
np.full((2, 3), 7) # filled with constant
np.eye(4) # identity matrix
np.arange(0, 10, 2) # [0, 2, 4, 6, 8]
np.linspace(0, 1, 5) # 5 evenly spaced: [0, .25, .5, .75, 1]
np.random.rand(3, 3) # uniform [0, 1)
np.random.randn(3, 3) # standard normal
np.random.randint(0, 10, size=5) # 5 ints in [0, 10)
np.arange mirrors Python's built-in range but returns an array instead of a lazy iterator, and it accepts floats. np.linspace is the one to reach for when you need an exact number of evenly spaced points rather than a fixed step size — the classic use case is generating x-axis values for a plot.
The dtype system: NumPy's real type system
Every NumPy array has exactly one dtype, and that single fact is both the source of NumPy's speed and the most common source of bugs for people coming from plain Python. Python lists tolerate mixed types freely; a NumPy array does not, by design.
a = np.array([1, 2, 3])
a.dtype # dtype('int64') on most 64-bit systems, int32 default on Windows
b = np.array([1, 2, 3], dtype=np.float32)
b.dtype # dtype('float32')
c = np.array([1, 2.5, 3])
c.dtype # dtype('float64') — NumPy upcasts the whole array
NumPy's core dtypes map directly onto C types: int8 through int64 (and unsigned uint8–uint64), float16/float32/float64, complex64/complex128, and bool_. Choosing the smallest dtype that fits your data range is one of the cheapest performance and memory wins available — an array of 10 million float32 values uses half the RAM of the same array stored as float64, which matters directly for cache locality and, by extension, speed.
One platform quirk worth flagging explicitly: on Windows, NumPy's default integer type has historically been 32-bit even on 64-bit machines, while Linux and macOS default to 64-bit. NumPy 2.0, released in June 2024 as the project's first major version bump since 2006, tightened and clarified these promotion rules specifically because they had been a recurring source of platform-dependent bugs (NumPy 2.0.0 Release Notes, numpy.org).
What happens when you mix types in an array constructor?
NumPy applies "type promotion": it finds the smallest common dtype that can represent every value without loss, then casts the whole array to it. Mixing an int and a float produces an all-float64 array; mixing a number with a string produces an dtype=object array that silently loses all of NumPy's speed advantages, because it's back to storing pointers to Python objects.
Array attributes and memory layout
a = np.array([[1, 2, 3], [4, 5, 6]])
a.shape # (2, 3)
a.ndim # 2
a.size # 6
a.dtype # int64
a.nbytes # 48 (6 * 8 bytes)
a.strides # (24, 8) — bytes to step to next row, next column
strides is the attribute most tutorials skip, but it's the mechanism that makes reshaping and transposing "free." A stride tuple tells NumPy how many bytes to jump in memory to move one step along each axis. For the 2x3 array above, moving one row down means jumping 24 bytes (three int64 values), while moving one column right means jumping 8 bytes (one value). Reshape and transpose operations generally just recompute the strides and shape metadata — they don't touch the underlying data buffer at all.
a.reshape(3, 2) # 3x2 VIEW of the same memory, no data copied
a.reshape(-1) # flatten to 1D (-1 means "infer this dimension")
a.T # transpose — swaps strides, still zero-copy
This is genuinely different from resizing a Python list, which must allocate new storage and copy references. A NumPy reshape on a contiguous array is an O(1) metadata operation regardless of how many elements the array holds, which is why reshaping a 10-million-element array and a 10-element array take the same, near-instant amount of time.
Indexing and slicing
NumPy supports three distinct indexing modes, and knowing which one you're using at any moment matters because they behave differently with respect to copying.
a = np.arange(12).reshape(3, 4)
# [[ 0 1 2 3]
# [ 4 5 6 7]
# [ 8 9 10 11]]
# Basic slicing — returns a VIEW, not a copy
a[0] # first row
a[:, 1] # second column
a[1:, 2:] # bottom-right submatrix
a[::2] # every other row
# Fancy indexing — returns a COPY
a[[0, 2]] # rows 0 and 2
a[[0, 1, 2], [1, 2, 3]] # diagonal-style: a[0,1], a[1,2], a[2,3]
# Boolean indexing
a[a > 5] # all elements > 5, returned flat as 1D
a[a % 2 == 0] = -1 # set all even values to -1, in place
The single most common NumPy bug: basic slicing (using : and integers) returns a view that shares memory with the original array. Modifying the slice modifies the source array too. Fancy indexing (using a list or array of indices) and boolean indexing both return independent copies. If you need an independent array from a basic slice, call .copy() explicitly:
row = a[0] # view — shares memory with a
row_safe = a[0].copy() # independent copy
row[0] = 999 # this also changes a[0, 0]!
Think of a view like a nickname for an existing house address — changing the mail delivered to "Bob's place" changes what arrives at the underlying street address, because they're the same location. A copy is like building a new house with an identical floor plan; changes to one never touch the other.
Broadcasting — the mechanism, not just the magic
Broadcasting is the rule set that lets NumPy apply operations between arrays of different shapes without you writing an explicit loop. Two array shapes are compatible for broadcasting if, comparing dimensions from the rightmost axis inward, each pair of dimensions is either equal or one of them is 1.
a = np.array([[1, 2, 3], # shape (2, 3)
[4, 5, 6]])
b = np.array([10, 20, 30]) # shape (3,)
a + b
# [[11 22 33]
# [14 25 36]]
# b is conceptually "stretched" to shape (2, 3) — no memory is actually duplicated
Crucially, NumPy does not physically copy b into a (2, 3) array before adding. It uses the stride mechanism described earlier: for the stretched dimension, the stride is set to 0, so the same 24 bytes of b get re-read for every row. This means broadcasting a small array against a huge one costs almost nothing extra in memory, only in the number of arithmetic operations performed.
A practical case, centering a dataset's columns for standardization before feeding it into a model:
data = np.random.randn(100, 5)
centered = data - data.mean(axis=0) # mean has shape (5,), broadcasts over (100, 5)
The broadcasting rules, stated plainly:
- Align the two shapes starting from the rightmost dimension.
- If one shape has fewer dimensions, pad its shape with size-1 dimensions on the left.
- Any dimension of size 1 gets conceptually stretched to match the other array's size along that axis.
- If, after that, any pair of dimensions still disagrees (and neither is 1), NumPy raises a
ValueError: operands could not be broadcast together.
Why do broadcasting errors mention shapes that look unrelated to my code?
Because NumPy reports the final, already-aligned shapes, not your original variable names. A (100, 5) array plus a (3,) array fails because 5 and 3 don't match and neither is 1 — the error message shows exactly those two numbers, which can look disconnected from the DataFrame or list you started with. Print .shape on every operand when debugging.
Vectorization vs. explicit loops
The cardinal rule of writing fast NumPy code is simple to state and easy to forget under deadline pressure: don't write a Python for loop that iterates over array elements.
# BAD — defeats the purpose of using NumPy at all
out = np.zeros(len(a))
for i in range(len(a)):
out[i] = np.sqrt(a[i]) + 1
# GOOD — vectorized, shorter, and dramatically faster
out = np.sqrt(a) + 1
The loop version pays Python's interpreter overhead on every single iteration — attribute lookups, bounds checks, and the generic-object arithmetic path — for what is, underneath, identical math. The vectorized version dispatches once into a compiled C loop (called a "universal function," or ufunc, in NumPy's own terminology) that runs without re-entering the Python interpreter at all.
NumPy ships element-wise ("universal function") implementations of nearly every standard math operation: np.sqrt, np.exp, np.log, np.sin, np.cos, np.abs, np.maximum, np.minimum, np.clip, and dozens more, all of which apply to every element of an array in one call and support broadcasting automatically.
How much faster is vectorized NumPy than a Python loop, concretely?
For simple elementwise math on arrays of a few hundred thousand elements or more, benchmarks in NumPy's own documentation and in the Harris et al. 2020 Nature paper consistently put vectorized operations at 50 to 200 times faster than the equivalent hand-written Python loop, with the gap widening as array size grows because loop overhead is fixed per element while the compiled path amortizes better.
Aggregations and the axis parameter
a = np.arange(12).reshape(3, 4)
a.sum() # 66
a.sum(axis=0) # column sums: [12, 15, 18, 21]
a.sum(axis=1) # row sums: [6, 22, 38]
a.mean(axis=0) # column means
a.std()
a.min(); a.max()
a.argmax(axis=1) # index of the max value in each row
The axis parameter confuses almost everyone the first time. A useful mnemonic: axis=0 means "collapse along rows" — you get one result per column, because you walked down each column. axis=1 means "collapse along columns" — one result per row. If the output shape still looks wrong, check it explicitly: a.sum(axis=0).shape should equal (a.shape[1],).
The modern random module
Since NumPy 1.17, the recommended way to generate random numbers is the Generator API via default_rng, not the legacy np.random.rand/np.random.randn functions, which remain for backward compatibility but are considered legacy by NumPy's own documentation.
rng = np.random.default_rng(seed=42) # modern API
rng.random((3, 3)) # uniform [0, 1)
rng.standard_normal((3, 3)) # N(0, 1)
rng.integers(0, 10, size=5) # random ints
rng.choice(['a', 'b', 'c'], size=4, p=[0.5, 0.3, 0.2])
rng.shuffle(arr) # in place
Always seed your generator when you need reproducible experiments — a fixed seed guarantees the identical sequence of "random" numbers on every run, which matters enormously for debugging a model training pipeline or reproducing a colleague's result. The new Generator API also uses a better underlying algorithm (PCG64 by default) than the legacy Mersenne Twister-based global state, with fewer statistical quirks at scale, per NumPy's random-sampling documentation.
Linear algebra
NumPy ships a linear algebra module (numpy.linalg) backed by BLAS and LAPACK, the same battle-tested Fortran libraries that underpin MATLAB, R, and most scientific computing environments.
A = np.array([[1, 2], [3, 4]])
b = np.array([5, 6])
A @ b # matrix-vector product (the preferred operator since Python 3.5 / PEP 465)
np.dot(A, b) # equivalent, older syntax
np.linalg.inv(A) # matrix inverse
np.linalg.det(A) # determinant
np.linalg.solve(A, b) # solve Ax = b directly — faster and numerically more stable than inv(A) @ b
np.linalg.eig(A) # eigenvalues and eigenvectors
np.linalg.svd(A) # singular value decomposition
np.linalg.norm(b) # L2 norm by default
np.linalg.solve deserves special emphasis because it's routinely skipped by beginners in favor of np.linalg.inv(A) @ b. Explicitly inverting a matrix is both slower and less numerically stable than solving the linear system directly, particularly as matrices grow large or become close to singular (ill-conditioned) — this is standard numerical linear algebra guidance echoed throughout NumPy's own documentation and textbooks like Golub and Van Loan's Matrix Computations. For least-squares fits on overdetermined systems, np.linalg.lstsq is the correct tool rather than manually forming normal equations.
Where the speed actually comes from: BLAS, LAPACK, and SIMD
It's worth being precise about why linear algebra operations in particular are so fast, because it's a different mechanism from the elementwise ufunc speedup described earlier. Matrix multiplication (A @ B) doesn't call a generic NumPy loop at all — it calls out to whatever optimized BLAS (Basic Linear Algebra Subprograms) library NumPy was built against, commonly OpenBLAS or Intel's MKL. These libraries use decades of algorithmic tuning: cache-blocking to keep data in fast CPU cache, multithreading across cores, and hand-written assembly kernels for specific CPU architectures.
This matters practically: two NumPy installations with identical Python code can differ in matrix-multiply speed by several times depending on which BLAS backend got linked at install time. If matrix multiplication feels unexpectedly slow, checking np.show_config() to see which BLAS library is active is a genuinely useful diagnostic step before assuming the code itself is the bottleneck.
Structured arrays and memory-mapped files
Two lesser-known NumPy features solve specific, real problems that come up constantly in production data work.
Structured arrays let a single ndarray hold named, mixed-type fields per row, similar in spirit to a lightweight, fixed-schema table:
dt = np.dtype([('name', 'U10'), ('age', 'i4'), ('weight', 'f8')])
people = np.array([('Alice', 30, 65.5), ('Bob', 25, 72.1)], dtype=dt)
people['age'] # array([30, 25])
This is the mechanism pandas itself is partly built on for certain operations, though pandas' DataFrame is generally the better tool once you need real indexing, joins, or missing-value handling — see the pandas guide for that layer.
Memory-mapped arrays (np.memmap) let you work with an array stored on disk as if it were in RAM, loading only the pages you actually touch:
big_array = np.memmap('data.dat', dtype='float32', mode='r', shape=(10_000_000, 50))
subset = big_array[1000:2000] # only these rows get read from disk
This is the standard technique for working with datasets larger than available RAM, common in genomics, geospatial raster processing, and large-scale simulation output, without needing a distributed computing framework.
NumPy vs. Python lists
| Feature | Python list | NumPy ndarray |
|---|---|---|
| Element type | Any (mixed allowed) | Fixed dtype (homogeneous) |
| Memory layout | Pointers to objects | Contiguous bytes |
| Element-wise math | Manual loop | Native (a + b) |
| Speed (numeric) | 1x baseline | ~50-200x faster |
| Multi-dimensional | Nested lists, awkward | Native, ergonomic |
| Memory overhead | ~28 bytes per int | 8 bytes per int64 |
| Append / grow | O(1) amortized | Expensive (must reallocate) |
Table: NumPy trades Python's flexibility for speed and memory efficiency on numeric data; lists remain the right tool for small, mixed-type, frequently growing collections.
Lists still win in three specific situations: mixed-type data, collections that grow one element at a time in a tight loop, and small collections where the fixed overhead of NumPy's dtype machinery isn't worth paying. For anything numeric and bulk, NumPy wins decisively.
NumPy vs. PyTorch tensors and TensorFlow: where the line actually sits
Because PyTorch tensors and TensorFlow tensors deliberately mirror much of NumPy's API — slicing, broadcasting, and most of the ufunc names are near-identical — it's easy to assume they're interchangeable. They aren't, and the differences matter for choosing the right tool.
PyTorch and TensorFlow tensors add two capabilities NumPy arrays fundamentally lack: GPU/TPU execution and automatic differentiation (the mechanism that computes gradients for training neural networks). If your workload doesn't involve a neural network or doesn't need a GPU, NumPy is lighter to import, has no CUDA driver dependency, and is generally faster to start up for small, CPU-bound scripts. If you're training a model, PyTorch's torch.Tensor (or TensorFlow's equivalent) is the correct data structure because it tracks the computation graph NumPy has no concept of.
A practical rule that holds up in most real pipelines: use NumPy for data preparation and feature engineering on CPU, hand off to pandas for anything tabular with labels, and only move into PyTorch tensors at the point where you're actually feeding a model — converting is a single call, torch.from_numpy(arr), and it shares memory with the original array rather than copying it.
The array API standard and NumPy 2.0
In June 2024, NumPy released version 2.0 — its first major version bump since 2006 — bringing full support for the Python array API standard (a specification, versioned 2022.12 and later, designed by a consortium including the NumPy, PyTorch, and CuPy maintainers to make array code portable across libraries) (NumPy 2.0.0 Release Notes, numpy.org). Practically, this means code written against the standard's subset of NumPy's API can run largely unmodified against CuPy (GPU arrays), JAX, or PyTorch, without library-specific branching. NumPy 2.0 also tightened scalar type-promotion rules that had previously produced surprising results — for example, adding a Python int to a float32 array no longer silently upcasts to float64 in every case, a change that broke some old code but fixed a long-standing class of bugs.
A practical mini-example: outlier detection with z-scores
Here's a compact, realistic use of most of the concepts above chained together — z-score normalizing a dataset, then isolating rows with any extreme value:
import numpy as np
data = np.random.randn(1000, 3) * [2, 5, 10] + [1, 0, -3]
mu, sigma = data.mean(axis=0), data.std(axis=0)
z = (data - mu) / sigma
outliers = data[np.abs(z).max(axis=1) > 2]
print(f"Found {len(outliers)} outliers out of {len(data)}")
Five lines, no explicit loop. Broadcasting handles the per-column mean and standard deviation subtraction automatically across all 1,000 rows; boolean indexing filters the rows in a single pass. Doing the equivalent with nested Python loops over rows and columns would take roughly 15–20 lines and run measurably slower on any dataset beyond a few thousand rows.
Common mistakes and what actually causes them
Chained assignment silently doing nothing. a[a > 5][0] = 0 looks like it should zero out the first element greater than 5, but a[a > 5] returns a copy (boolean indexing), so the assignment modifies a throwaway array and a itself is unchanged. Use a[np.where(a > 5)[0][0]] = 0 or restructure the mask logic to assign in one indexing step, like a[a > 5] = 0 if you want to zero all of them.
Comparing floats for exact equality. Because of floating-point representation error, 0.1 + 0.2 == 0.3 is False even in NumPy. Use np.isclose(a, b) or np.allclose(a, b), which compare within a small tolerance, for any float comparison.
Forgetting that in-place operators mutate views. a[:5] += 1 modifies the original array through the view, which is often intended — but if you passed that slice into a function expecting an independent array, the caller's data has silently changed too. This is a frequent source of hard-to-trace bugs in larger codebases.
Mixing axis=0 and axis=1 under time pressure. There's no shortcut here beyond checking .shape after every aggregation until the mnemonic ("axis=0 collapses rows, walks down columns") becomes automatic.
FAQ
Q: Should I learn NumPy before pandas? A: Yes. pandas is built on NumPy, and understanding arrays, dtypes, views versus copies, and broadcasting makes pandas dramatically easier to reason about. See our pandas guide next.
Q: NumPy vs. PyTorch tensors — what's the real difference? A: PyTorch tensors carry an API almost identical to NumPy arrays but add GPU execution and automatic differentiation for training neural networks. For CPU-only numeric work with no model training involved, NumPy is lighter and faster to import.
Q: Why is a[i, j] better than a[i][j]?
A: a[i][j] creates an intermediate one-dimensional array for row i, then indexes into that. a[i, j] is a single indexing operation handled entirely inside NumPy's C code — faster, and it also works correctly for advanced indexing patterns that a[i][j] cannot express.
Q: How do I save and load arrays efficiently?
A: np.save('arr.npy', a) and np.load('arr.npy') for a single array; np.savez for multiple arrays in one file. np.savetxt writes plain-text CSV for interoperability but is substantially slower and larger on disk than the binary .npy format.
Q: Why does my array suddenly become dtype=object?
A: You mixed incompatible types (strings and numbers) or passed a ragged nested list (rows of different lengths). Object arrays store Python object pointers rather than raw values, which erases essentially all of NumPy's speed advantage. Fix the source data or switch to pandas, which handles heterogeneous, labeled data more gracefully.
Q: When should I not use NumPy at all? A: When your data is heterogeneous with labels (use pandas), when you need relational queries across tables (use SQL), or when the computation is fundamentally sequential and can't be expressed as array operations, such as a recursive algorithm with data-dependent branching at every step.
Q: What's the difference between a view and a copy, and how do I check which one I have?
A: A view shares the same underlying memory as its source array; a copy is fully independent. Basic slicing (:) returns views; fancy and boolean indexing return copies. Check with arr.base is None — a view's .base attribute points back to the original array, while an independently-owned array's .base is None.
Q: Is NumPy thread-safe for parallel processing?
A: Most read operations are safe across threads, and many linear algebra and ufunc operations release Python's Global Interpreter Lock (GIL) internally, allowing genuine multi-core speedup even in plain threading. Concurrent in-place writes to the same array from multiple threads, however, are not automatically synchronized and can produce race conditions — use explicit locks or process-based parallelism (multiprocessing) for that case.
Q: Do I need to learn NumPy if I only plan to use deep learning frameworks? A: Largely yes. Data loading, preprocessing, and evaluation code in nearly every PyTorch or TensorFlow project still passes through NumPy arrays at some stage, and the mental model (broadcasting, views versus copies, dtype promotion) transfers directly to tensor operations in both frameworks.
Q: What changed in NumPy 2.0 that I should actually care about? A: Full support for the array API standard (portable code across NumPy, CuPy, JAX, and similar libraries), stricter and more predictable scalar type-promotion rules, and a new string dtype. Code that relied on the old, looser promotion behavior may need small adjustments when upgrading (NumPy 2.0.0 Release Notes, numpy.org).
The takeaway
NumPy's entire value proposition traces back to one design choice: storing numeric data as contiguous, fixed-type bytes instead of Python's default scattered-object model, then handing the actual arithmetic to pre-compiled, vectorized C and Fortran code. Everything downstream — broadcasting, the 50-to-200x speedups, the view-versus-copy distinction, and even why PyTorch's API looks the way it does — follows from that single decision. Learn the array model here and pandas, scikit-learn, and PyTorch all get noticeably easier, because they're built on the same foundation rather than fighting against it.
Read next:
- SQL for Beginners — Query Databases Like a Pro
- Python Pandas Data Analysis Guide
- Complete Machine Learning Guide 2026
Sources
- NumPy Documentation — numpy.org/doc/stable
- Harris, C.R., Millman, K.J., van der Walt, S.J. et al. (2020). "Array programming with NumPy." Nature 585, 357–362. doi.org/10.1038/s41586-020-2649-2
- NumPy 2.0.0 Release Notes — numpy.org/doc/stable/release/2.0.0-notes.html
- Python Array API Standard — data-apis.org/array-api
- Van Der Walt, S. et al. (2011). "The NumPy Array: A Structure for Efficient Numerical Computation." Computing in Science & Engineering, 13(2), 22–30.
- "Python for Data Analysis," 3rd ed. — Wes McKinney, O'Reilly, 2022
Follow new articles
Add SciHub101 to your feed reader to see new science and technology stories.
About the Author
SciHub101 is an independent science and technology publication founded and edited by Buğra Sözeri, a software engineer and app developer based in Türkiye. AI tools assist with drafting and research. Source documentation in the existing archive is uneven, and articles are being reviewed and corrected. The Editorial Team byline identifies editorial responsibility; it does not certify specialist or independent human review. Where we compare products, anything built by our own team is labelled as such — see the editorial guidelines and About page for how corrections and disclosures work.
Related Articles

SQL for Beginners — Query Databases Like a Pro
Learn SQL from scratch — SELECT, JOINs, GROUP BY, window functions, and indexes. Practical examples for PostgreSQL, MySQL, SQLite, and BigQuery.

Python for Data Science: Complete Guide 2026
Master Python for data science in 2026. Covers essential libraries, workflows, career paths, and learning roadmap with real examples.

Python Data Libraries Cheat Sheet 2026 — Quick Reference
Quick reference for Python data libraries 2026: Pandas, Polars, DuckDB, NumPy, Plotly. Common operations side-by-side. Bookmark for daily work.

Pandas Python Guide — Data Analysis for Beginners
Learn pandas: DataFrames, loading, cleaning, groupby, merging, and plotting, with pandas 3.0 code examples and a full CSV-to-insight analysis pipeline.

Polars vs Pandas 2026 — Should You Make the Switch?
Polars vs Pandas compared: real benchmarks, syntax differences, migration strategy. Why Polars is often 10 to 20x faster and when switching still isn't worth it.

DuckDB vs Pandas — Which to Use for Data Analysis 2026
DuckDB vs Pandas: when to use each, performance benchmarks, SQL vs Python syntax, real examples. DuckDB is faster and handles larger data — should you switch?