Quant Memo
Foundational

NumPy Arrays and Dtypes

A NumPy array is a single block of same-typed memory laid out for fast bulk math, and its dtype decides how many bytes each element costs and how it can silently overflow or round.

Prerequisites: Floating-Point Arithmetic

A native Python list of a million floats is not a million floats sitting next to each other in memory. It's a million separate Python objects scattered across the heap, and the list itself just holds pointers to them. Every element carries its own type tag, its own reference count, its own overhead — a Python float that "is" 8 bytes of data actually costs around 24 bytes once you include that bookkeeping. Doing arithmetic on the list means chasing a million pointers and unwrapping a million objects. NumPy's answer is to throw all of that away for the case where every element is the same type: store a million raw numbers back-to-back in one contiguous block of memory, and describe the whole block with one shared type descriptor instead of a million.

The idea: a dtype is the label on the box

An array is a flat buffer of bytes plus some metadata: its shape (how many elements along each axis) and its dtype (data type — how many bytes each element takes and how to interpret them: signed integer, unsigned integer, floating point, at what width). Every element in the array shares that one dtype. That uniformity is exactly what makes bulk operations fast: the CPU can walk the buffer in fixed-size steps, never has to check "what type is this element" per element, and can load many elements into a cache line at once (The CPU Cache Hierarchy) because they're genuinely adjacent in memory, unlike a Python list's scattered pointers.

The dtype is not a suggestion — it is a hard contract about how many bits are available to hold each value, and it enforces that contract silently. This is where NumPy differs sharply from plain Python, where integers grow arbitrarily large automatically.

Worked example: silent overflow with int8

Create an array of 8-bit signed integers, int8, which can represent values from -128 to 127 (2⁸ = 256 distinct values, split roughly evenly around zero).

import numpy as np

prices = np.array([100, 120, 127], dtype=np.int8)
prices + 5
# array([ 105,  125, -124], dtype=int8)

Trace the last element by hand. 127 + 5 = 132 in ordinary arithmetic. But int8 has no room for 132 — its largest representable value is 127. The addition wraps around: think of the eight bits as an odometer that rolls over. 132 in binary needs 8 bits as 10000100, and because int8 treats the leading bit as the sign, that pattern is interpreted as a negative number: specifically 132256=124132 - 256 = -124. The array silently reports -124, no error, no warning. A position size or a cumulative P&L computed in int8 or even int32 can overflow the same way if the numbers involved grow larger than the analyst anticipated — the fix is picking a dtype with enough headroom (int64, or a float type) for the largest value the computation will ever see, not hoping it won't happen.

np.array([127], dtype=np.int8) + np.array([5], dtype=np.int8)
# array([-124], dtype=int8)   <-- wrapped, no exception raised
np.array([127], dtype=np.int64) + np.array([5], dtype=np.int64)
# array([132])                <-- correct, plenty of headroom

Worked example: dtype promotion when mixing types

When an operation combines two different dtypes, NumPy promotes to a common type wide enough to hold either operand without losing information, following a fixed hierarchy: boolean < integer < float, and within each, wider always beats narrower.

a = np.array([1, 2, 3], dtype=np.int32)
b = np.array([1.5, 2.5, 3.5], dtype=np.float32)
(a + b).dtype
# dtype('float64')

Trace why the result is float64 and not float32. NumPy's promotion rules treat int32 combined with float32 as needing more precision than float32 alone can guarantee for every possible int32 value (an int32 can represent exact integers up to about 2.1 billion, while float32 only has about 7 decimal digits of precision — see Floating-Point Arithmetic — so some large int32 values would lose exactness if forced into float32). NumPy resolves that by promoting to float64, which has enough mantissa bits to hold both faithfully. This matters in practice because promotion changes memory footprint on the fly: an array you built as float32 to save memory can silently double in size the moment it's combined with an int64 column, which is a common source of an unexplained memory blow-up when merging price data (often float32 or float64) with volume or contract-count data (often int64).

Python list pointers 5 2 9 each object scattered, own overhead

NumPy array (int32) 5 2 9 1 7 one contiguous buffer, 4 bytes each, one shared dtype

A list stores pointers to scattered objects; a NumPy array stores the raw values themselves, back to back, all interpreted through one dtype.

A dtype is a fixed-width contract, not a suggestion: it fixes how many bytes each element gets and how those bytes are interpreted, which is what makes bulk array math fast, and it is also exactly why arrays overflow and lose precision silently instead of growing or raising an error the way plain Python numbers do.

Where this shows up

In a quant-dev interview, dtype questions usually probe whether you know why np.array([1,2,3]) / 2 behaves differently from integer division in some other language, or why a cumulative sum of int32 P&L ticks can quietly go negative after enough trades. In production, dtype choice is a real memory and correctness lever: tick-level order book data is often stored as float32 or even fixed-point integers (price in ticks, an integer, rather than a float) specifically to halve memory bandwidth and avoid floating-point comparison bugs, while risk aggregation code deliberately upcasts to float64 or int64 before summing to avoid the overflow and precision problems traced above.

np.array([1, 2, 3]) / np.array([2, 2, 2]) on integer arrays returns floats in NumPy by default (true division), but // forces integer (floor) division and silently truncates — a frequent source of off-by-fraction bugs when converting a formula from math notation straight into code without checking which operator was actually used.

Related concepts

Practice in interviews

Further reading

  • Harris et al., Array Programming with NumPy, Nature 585 (2020)
  • NumPy documentation, Data type objects (dtype)
ShareTwitterLinkedIn