NumPy12/18
Starting sandbox…
Beginner·4 graded tests

NumPy dtypes

NumPy dtypes

Core Principle: An array data type (dtype) dictates element bit-width, memory consumption, precision limits, and numerical overflow behaviors across computational operations.

The families

Family Examples Notes
Signed int int8 int16 int32 int64 Wraps silently on overflow
Unsigned int uint8 uint16 uint32 uint64 Mixing with signed promotes to float
Float float16 float32 float64 float64 is the default
Complex complex64 complex128 Rare in bio
Bool bool_ One byte per element, not one bit
Fixed string <U10, S10 Unicode / bytes, fixed width: truncates!
Object object Python objects in a box; kills all performance
Datetime datetime64[D], timedelta64[ns] Unit is part of the dtype
Structured see Structured Arrays Named fields, C-struct-like
[1]python
out1ms
array([1., 2., 3.], dtype=float32)

Precision, memory and the real tradeoff

dtype Bytes Range / precision
int8 1 −128 … 127
int16 2 ±32,767
int32 4 ±2.1 × 10⁹
int64 8 ±9.2 × 10¹⁸
float16 2 ~3 decimal digits
float32 4 ~7 decimal digits
float64 8 ~16 decimal digits

A 20,000 × 500 matrix: 80 MB as float64, 40 MB as float32. For expression counts, coverage, or quality scores, float32 is plenty: 7 significant digits far exceeds the measurement precision of any assay. For accumulating sums over millions of elements, keep float64, because rounding error accumulates.

counts.astype(np.float32)                     # halve the memory
counts.sum(axis=0, dtype=np.float64)          # but accumulate in float64

That dtype= argument on reductions is underused and exactly right: low-precision storage, high-precision accumulation.

Overflow is silent and it will get you

[2]python
out1ms
array([-2], dtype=int32)

Genomic coordinates on chromosome 1 (~249 Mb) fit in int32, but a whole-genome cumulative offset (~3.1 Gb) does not. Coverage totals across a large cohort do not. Use int64 for anything positional or cumulative.

Python's own ints are arbitrary-precision, so this is a NumPy-specific hazard that people migrating from pure Python do not expect.

NEP 50: what changed in NumPy 2

Scalar promotion rules changed. Under NumPy 2:

[3]python
error1ms
np.uint8(44)
<cell>:2: RuntimeWarning: overflow encountered in scalar add

The rule is now: a Python scalar adopts the array's dtype rather than promoting it. This is more predictable, but it means numerical results can differ between NumPy 1 and 2 in low-precision code. If you have old float32 pipelines, re-verify their output. See Versions and Compatibility.

The fixed-string trap

[ ]python
out1ms

This code completed without producing output.

The width is baked in at creation. This is one of the reasons genomic labels belong in Pandas (which now has a proper str dtype) rather than NumPy string arrays.

The object dtype is a warning sign

[5]python
out1ms
array([1, 'a', None], dtype=object)

An object array is a Python list wearing an ndarray costume: every operation goes through the interpreter, so you lose all of NumPy's speed and most of its functions. If you land in object dtype, you almost certainly want a DataFrame.

Checking and converting

np.issubdtype(a.dtype, np.integer)
np.issubdtype(a.dtype, np.floating)
a.astype(np.int32, casting="safe")     # raises rather than silently truncating
np.can_cast(np.int64, np.int32)        # False

casting="safe" is a good habit in pipeline code: it turns a silent data corruption into an exception.

Common mistakes

  • Integer division surprises. np.array([5]) / 2 gives 2.5 (float), but // 2 gives 2 and stays integer. In-place arr /= 2 on an int array raises.
  • np.nan in an integer array. Impossible: NaN is a float concept. The array is silently promoted to float, or the assignment fails. See Missing Data in NumPy.
  • Assuming int means int64 everywhere. On Windows, the default integer was historically int32. Be explicit in cross-platform code.
  • float16 for real work. It has about 3 decimal digits. Fine for storage of already-noisy data, dangerous for computation.
  • Forgetting astype returns a copy. a.astype(np.float32) does not modify a.

See also

ndarray · Missing Data in NumPy · Structured Arrays · pandas dtypes · Memory Layout and Strides · Versions and Compatibility

Exercise

Write pack(values), which narrows a list of Python integers to np.int8 and reports what that costs. Return ["empty"] for an empty list.

Otherwise return five lines: overflow=<n>/<total>, counting the values outside the closed range -128 to 127; wrapped=<list>, the result of casting an int64 array straight to int8, which silently wraps around rather than raising; clipped=<list>, the result of clipping to the int8 range first and then casting, which saturates instead; promoted=<name> widened=<name>, the dtype names np.result_type gives for the int8 array combined with np.uint8 and with np.float32: the first is wider than either input, which surprises most people; and bytes=<n> vs <n>, the int8 array's nbytes against the int64 array's. Report dtypes by .name and never by their repr.

exerciseEvery submission runs independently in a fresh namespace.

Test cases · 4

#viainputexpected stdout
1stdin10 200 -5 128 -129 127overflow=3/6 wrapped=[10, -56, -5, -128, 127, 127] clipped=[10, 127, -5, 127, -128, 127] promoted=int16 widened=float32 bytes=6 vs 48
2stdin0 1 2 3overflow=0/4 wrapped=[0, 1, 2, 3] clipped=[0, 1, 2, 3] promoted=int16 widened=float32 bytes=4 vs 32
3stdin-128overflow=0/1 wrapped=[-128] clipped=[-128] promoted=int16 widened=float32 bytes=1 vs 8
4stdin·empty

Hints · 2

01Hint

np.clip(arr, -128, 127).astype(np.int8) saturates, while arr.astype(np.int8) alone wraps modulo 256.

02Hint

np.result_type(np.int8, np.uint8) has to hold every value of both, so neither 8-bit type is wide enough.