DS in Python 7 — Numpy Internals (ndarray, strides, Views vs copy, C-Order and Fortran-Order, ufuncs, broadcasting)
NumPy is one of the most important libraries in Python for numerical computing. It is widely used in data science, machine learning, scientific computing, and data analysis.
1. The Core of NumPy: ndarray
The most important object in NumPy is the ndarray, which means N-dimensional array.
arr = np.array([10, 20, 30, 40])A NumPy array is not simply a Python list. Internally, it stores:
- The actual data
- Data type (
dtype) - Shape
- Strides
- Information about the memory containing the data
You can see some of this information using:
print(arr.dtype)
print(arr.shape)
print(arr.strides)Example:
import numpy as np
arr = np.array([
[1, 2, 3],
[4, 5, 6]
])
print(arr.dtype)
print(arr.shape)
print(arr.strides)
'''OUTPUT:
int64
(2, 3)
(24, 8) # 24 bytes to move to next row, 8 bytes to move to next column
'''2. NumPy Stores Data Efficiently
One major difference between a Python list and a NumPy array is how the data is represented.
A Python list generally stores references to Python objects:
List
↓
Reference → Python object
Reference → Python object
Reference → Python objectA NumPy array generally stores elements of a fixed data type in a compact data buffer:
NumPy Array
┌────┬────┬────┬────┐
│ 10 │ 20 │ 30 │ 40 │
└────┴────┴────┴────┘For example, if the array has int32 values, each value occupies 4 bytes.
This predictable representation reduces memory overhead and allows NumPy to process numerical data efficiently.
3. dtype: Why Data Type Matters
Every NumPy array has a data type.
arr = np.array([1, 2, 3], dtype=np.int32)
print(arr.dtype)Output:
int32You can also use types such as:
np.int64
np.float32
np.float64The dtype determines how each element is represented in memory.
For example:
np.int32uses 4 bytes per element, while:
np.int64uses 8 bytes.
Therefore, choosing an appropriate data type can make a significant difference when working with very large arrays.
ndim→ number of dimensionsshape→ size of each dimensiondtype→ type of each elementsize→ total number of elements
4. Strides: How NumPy Finds Data
Strides are one of the most important concepts in NumPy internals.
Strides tell NumPy how many bytes it needs to move in memory when moving to the next element along each dimension.
For example:
arr = np.array([
[1, 2, 3],
[4, 5, 6]
], dtype=np.int32)
print(arr.strides) # (12, 4)12 because 12 bytes to move to next row, 4 because 4 bytes to move to the next column
You don’t normally need to calculate strides yourself, but they explain how operations such as slicing, reshaping, and transposing can work efficiently.
5. Views vs Copies
This is one of the most important NumPy concepts.
A view is another way of looking at existing data. It can share the same underlying memory.
For example:
arr = np.array([10, 20, 30, 40])
view = arr[1:3]
print(view) # [20,30]
view[0] = 99
print(arr) # [10 99 30 40]The original array changed because the slice can be a view of the same data.
If you want completely separate data, use:
copy = arr[1:3].copy()Now changes to copy won't modify the original array.
Why is this important?
Views can:
- Save memory
- Avoid unnecessary copying
- Make operations faster
But they can also cause unexpected changes if you don’t realize that two arrays share memory.
Reshaping Without Copying
Consider:
arr = np.arange(6)
new_arr = arr.reshape(2, 3)The result is:
[[0 1 2]
[3 4 5]]A reshape can often be done by changing the way NumPy interprets the existing data rather than copying all the values.
Get Abhishek Jain’s stories in your inbox
Join Medium for free to get updates from this writer.
However, not every reshape is guaranteed to return a view. Depending on the memory layout and requested shape, NumPy may need to create a copy.
This is why understanding views and memory layout is useful when working with large arrays.
C-Order and Fortran-Order
For a 2D array, the elements are stored in memory as one long linear sequence. The difference between C-order and Fortran-order is simply which dimension changes fastest in memory.
Multidimensional arrays can be laid out in memory in different ways.
The two common layouts are:
Array:
[[1, 2, 3],
[4, 5, 6]]
C-order (row-major)
-------------------
[1][2][3][4][5][6]
↑ row0 ↑ ↑ row1 ↑
Fortran-order (column-major)
----------------------------
[1][4][2][5][3][6]
↑ col0 ↑ ↑ col1 ↑ ↑ col2 ↑Universal Functions (ufuncs)
NumPy provides optimized functions called ufuncs, or universal functions.
Examples include:
np.add()
np.multiply()
np.sqrt()
np.sin()For example:
arr = np.array([1, 4, 9])
result = np.sqrt(arr)Output:
[1. 2. 3.]Instead of writing a Python loop, NumPy applies the operation to the array efficiently.
Broadcasting
Broadcasting is another important NumPy feature.
It allows NumPy to perform operations on arrays with compatible shapes.
For example:
arr = np.array([1, 2, 3])
print(arr + 10)Output:
[11 12 13]NumPy doesn’t need you to manually create:
[10, 10, 10]It applies the scalar to each element according to its broadcasting rules.
Broadcasting also works with multidimensional arrays:
a = np.array([
[1, 2, 3],
[4, 5, 6]
])
b = np.array([10, 20, 30])
print(a + b)Output:
[[11 22 33]
[14 25 36]]Broadcasting makes code shorter and can avoid unnecessary data duplication.
NumPy Uses Compiled Code
The Python code we write is only the interface.
For many operations, NumPy passes the work to compiled code that operates directly on the array’s data.
A simplified view is:
Python Code
↓
NumPy API
↓
Compiled NumPy Implementation
↓
Array DataThis reduces Python interpreter overhead and allows NumPy to perform numerical operations efficiently.
NumPy can also make use of optimized low-level numerical libraries and hardware-specific optimizations depending on the operation and installation.
Memory Sharing
NumPy arrays can share the same underlying memory.
np.shares_memory(a, b) checks whether two NumPy arrays share at least some of the same underlying memory.
You can check this using:
np.shares_memory(a, b)For example:
a = np.arange(10)
b = a[2:5]
print(np.shares_memory(a, b))This can return:
TrueUnderstanding memory sharing is useful when debugging unexpected changes and controlling memory usage.
Conclusion
NumPy’s performance comes from more than just being a Python library. Its core ndarray uses a structured representation of data with a fixed data type, shape, strides, and an underlying memory buffer.
Features such as views, broadcasting, vectorization, and ufuncs allow NumPy to process large amounts of numerical data efficiently.
You don’t need to understand every detail of NumPy’s internal implementation to use it. But understanding these basic concepts helps you avoid unnecessary copies, manage memory better, and write faster NumPy code.
The most important idea to remember is:
NumPy is fast because it stores numerical data efficiently and performs many operations in optimized compiled code instead of doing everything through Python-level loops.










