Dtypes

One type for the whole array - what it costs, when it overflows, and the promotion rules that decide the result.

Overview

Kind and width

Every element in a NumPy array has the same type. That type is the dtype. It records two things: what kind of number the element is, and how many bytes it occupies. Understanding dtypes is necessary because they control precision, memory usage, and the behaviour of arithmetic operations.

The kind tells you the family: signed integer, unsigned integer, floating point, complex, boolean, or string. The width tells you the size: int8 is one byte, int64 is eight bytes. A wider type holds a larger range of values but uses more memory.

Choosing the right dtype matters for two reasons. First, memory. A column of ages stored as int64 uses eight bytes per value. Stored as int8, it uses one byte. For a million rows, that is the difference between eight megabytes and one megabyte. Multiply by many columns and the savings determine whether the data fits in memory at all.

Second, overflow. An int8 holds values from negative 128 to positive 127. Adding two int8 values that sum to more than 127 wraps around silently. There is no error. The result is wrong. This happens in practice when people downcast aggressively without checking the value range.

Floating-point precision is the other common issue. float32 has about seven decimal digits of precision. float64 has about fifteen. Most scientific computation uses float64 by default. Switching to float32 halves the memory and speeds up some operations, but accumulated rounding errors can change the result. Whether that matters depends on the application.

NumPy infers the dtype from the input. A list of Python ints becomes int64. A list of Python floats becomes float64. A list with one float and the rest ints becomes float64, because the dtype must hold every element. That promotion is silent and worth checking.

This module covers the common dtypes, their ranges and memory costs, how to specify them, and how to convert between them safely. It also covers the structured dtypes that let a single array hold multiple fields per element, like a lightweight record type.

A dtype records what kind of number and how many bytes. int32 is a signed int in four bytes. float64 is a double-precision float in eight.

Both halves matter. The kind decides what steps mean. The width decides the range and the memory.

np.iinfo and np.finfo report the limits. It's worth looking at them once rather than remembering guesses. The first editor prints them.

Worth knowing

A dtype is a kind and a width. It is fixed when the array is created and applies to every element.
NumPy integers wrap on overflow rather than growing or raising. Python integers do the opposite, so ported code can start returning wrong numbers.
Promotion picks the narrowest type holding both operands — int64 + float32 gives float64, which is neither input.
Assignment converts the value to the array's dtype. It never widens the array, so floats truncate into integer arrays.
astype always copies and will lose information without complaint. Round before converting if that is what you meant.
float32 halves memory and keeps about seven significant digits — fine for weights and images, not for a long running sum.

Dtypes

One type for the whole array: what it costs, when it overflows. How promotion decides the result.

What a dtype records

A kind, a width, and therefore a range. Every element of the array uses it.

example_01.pyNumPy
Output

Integers wrap; they do not grow

This is the sharpest difference from Python, and it fails silently.

example_02.pyNumPy
Output

Promotion decides the result type

Combining two dtypes gives the narrowest type that can hold both. That is usually helpful and occasionally expensive.

example_03.pyNumPy
Output

Assignment cannot change the dtype

Writing into an array converts the value to fit. It never widens the array.

example_04.pyNumPy
Output

Converting on purpose

astype makes a new array with a new dtype. It always copies, and it will happily lose information.

example_05.pyNumPy
Output

Picking a width deliberately

Memory and range are the two axes. For large arrays the choice is worth making rather than defaulting.

example_06.pyNumPy
Output

Integers wrap

This is the sharpest gap from plain Python and the one most likely to cause a quiet bug.

A Python int grows to what size it needs. A NumPy int is a fixed-width machine int. Exceeding it wraps around. int8 at 127 plus one is −128. No exception, no warning.

Code that worked on lists can give wrong numbers on arrays. The failure is silent. That is worse than a crash. Nothing draws focus to it.

Two habits help. Choose a width with headroom when values can grow. And be aware that the default int is platform-dependent. int64 on most desktops, int32 on this page. So an example that is safe on your laptop may overflow elsewhere, and vice versa.

Promotion

When two arrays of other dtype meet, NumPy finds a type that can hold both and converts before operating.

Mostly this is what you want. int8 + int16 gives int16. int32 + float32 gives float64. This Surprises people the first time: float32 can't represent each int32 just. NumPy widens to a thing that can.

The result worth knowing is memory. A carefully chosen float32 array mixed with anything float64 makes a float64 result. A pipeline can quietly double its footprint at one careless line. If you're working in float32 on purpose, keep constants and mid arrays in float32 too.

Assignment converts, it does not widen

Writing into an array converts the value to the array's dtype:

ints = np.arange(5)
ints[0] = 9.99        # stores 9

Truncation toward zero, silently. The array's type was decided at creation and a single set can't change it.

The same applies to precision. Writing 1/3 into a float32 array stores the nearest float32. Reading it back gives a value that differs from Python's float in the seventh decimal place.

astype

astype makes a new array with a new dtype. Two things to note.

It always copies, even when the dtype is unchanged, unless you pass copy=False. That is a real cost on large arrays.

It converts without complaint. Floats truncate toward zero rather than rounding. 1.9 turns 1 and -1.9 turns -1. Values outside the target range overflow. If you meant to round, call np.round first and then convert.

Choosing a width

Two considerations, and they pull in opposite directions.

Memory. A million float64 values is 8 MB. In float32 it's 4 MB, in float16 2 MB. On large arrays this is the gap between fitting in cache and not. This Affects speed as much as room.

Precision and range. float32 carries about seven large decimal digits, float64 about sixteen. For image pixels, neural network weights and most measured data, float32 is ample. For building up a sum over millions of elements it isn't. The running total grows until adding one more small value changes nothing.

The last editor shows that: summing a million ones in float32 doesn't give a million. sum(dtype=np.float64) builds up in double precision while keeping the array itself small. This Is often the right compromise.

A working rule

Default to float64 for numeric work unless memory is a real constraint. Use float32 on purpose, for large arrays. Keep the whole pipeline in it.

For ints, pick a width that fits your values with room to spare. Be clear when the numbers might be large. The default isn't the same all over.

And when a result looks wrong by a factor of two, or minus when it shouldn't be, check the dtype before checking your logic.

What float precision actually buys you

float64 carries about 15 to 17 large decimal digits. float32 carries about 7. float16 carries about 3.

Those numbers are the whole story for choosing. The handy question isn't "how much precision is on hand" but "how much does my data have".

A temp sensor accurate to a tenth of a degree has three large digits. Storing its readings in float64 records twelve digits of noise very just. float32 would be ample, and would halve the memory.

The counter-arg is build-up. Summing a million float32 values loses precision faster than summing a million float64 values. The rounding error at each step is larger scaled to the running total. NumPy mitigates this by using pairwise summation inside. This Keeps the error growth much slower than a naive loop. It doesn't drop it.

A hands-on compromise, and what many libraries do. Store in float32, build up in float64. a.sum(dtype=np.float64) does just that. Reads a narrow array, builds up in a wide type.

Why 0.1 + 0.2 is not 0.3

Binary floating point can't represent 0.1 just, any more than decimal can represent one third just. The stored value is very slightly off, and the errors compound.

This isn't a NumPy issue. It's the same in plain Python, in C. In each language using IEEE 754. But it bites harder in array code because you're more likely to be checking results of long steps.

The result: never test floats for equality.

np.isclose(a, b) checks elementwise with a handling. np.allclose(a, b) reduces that to a single boolean. Is the right thing in a test.

Both take rtol and atol. The scaled handling handles large values. Here A fixed abs gap is meaningless. The abs handling handles values near zero. Here A scaled compare breaks down. The defaults are sensible for common data and worth replacing when your values are unusually large or unusually small.

equal_nan=True makes NaN compare equal to NaN. This Is often what you want when checking that two arrays came out the same.

Booleans

np.bool_ is one byte, not one bit. NumPy doesn't pack booleans. A mask over a million elements costs a megabyte.

Booleans promote to ints in math, with True turning 1. That is why mask.sum() counts matches. It's one of the more handy accidents of the type system.

They don't promote silently in the other direction. Assigning 2 into a boolean array stores True. Anything nonzero is true. That conversion is lossy and silent. Is a reason to be careful about which array you're assigning into.

Strings, and why they surprise people

NumPy string dtypes are fixed width. An array created from ["ana", "bartholomew"] gets dtype <U11, sized to the longest.

Assign a longer string into it later and it's silently truncated to eleven characters. Nothing raises. This is the single most surprising action in NumPy's type system. It's a direct result of the fixed-size-elements design that makes all else work.

If you need real var-length strings, the options are an object array. Which stores pointers to Python strings and gives up each speed gain. Or pandas. This Handles this properly.

NumPy 2.0 added a var-width string dtype that handles this. It's recent enough that most code and most settings you'll meet don't have it.

Object arrays

dtype=object makes an array of pointers to any Python objects. It looks like an array and supports the indexing syntax.

It isn't fast. each step falls back to calling Python methods per element. This Is a list with extra steps. It also can't be saved without pickling, with the security results covered later.

at times it's the right answer. A ragged set that you truly need to index like an array. often its appearance is a sign that the data doesn't want to be a NumPy array at all. It's worth checking whether a list, a dict or a DataFrame fits better before building on it.

Working rules

Set the dtype at creation. Check a.dtype when a result looks wrong. It's the second thing to look at after a.shape. One of them shows most surprises.

Use float32 when precision allows and memory matters, and build up in float64.

Never compare floats with ==. Use np.isclose or np.allclose.

Treat int arrays as capable of silently wrapping. Pick a width with headroom for the data you might see, not the data in front of you.

And treat a fixed-width string dtype as a fixed-width string dtype. Check the width before assigning into one.

Checking dtypes in practice

a.dtype is the direct question, and checking it works as you'd expect: a.dtype == np.float64.

For a group rather than an exact type, np.issubdtype(a.dtype, np.int) and np.issubdtype(a.dtype, np.floating) are the right tests. They handle each width at once. This A chain of equality checks doesn't.

a.dtype.kind gives a single character: i for signed int, u for unsigned, f for float, b for boolean, U for unicode string, O for object. It's convenient for quick branching.

In a function that accepts arrays from callers, converting defensively is often better than checking: a = np.asarray(a, dtype=float) accepts lists, int arrays and float arrays alike. Makes just one predictable type. It copies only when it has to.

The unsigned trap

Unsigned ints behave oddly in subtraction.

np.uint8(3) - np.uint8(5) isn't -2. There are no minus values in an unsigned type. It wraps to 254.

This bites most often with image data. This Is often uint8. Subtracting two images to find a gap gives large plus values wherever the result should have been minus. The resulting image looks wrong in a way that is easy to misread as a bug elsewhere.

The fix is to convert before subtracting: a.astype(np.int16) - b.astype(np.int16), then clip and convert back if needed.

Mixed signed and unsigned math also promotes in ways that surprise. Mixing int64 and uint64 gives float64. No int type can hold the full range of both. And that silent jump to float is a real source of precision loss on large ints.

Questions people ask about Dtypes

What is a dtype? It is the description of how each element's bytes are read: the kind, such as integer or float, and the width in bits. Every element in an array shares one dtype.

What happens when an integer array overflows? It wraps around silently. An int8 holding 127 plus 1 becomes -128, with no warning, so pick a width that holds your largest value.

What is the difference between astype and view? astype converts the values and returns a new array. view reinterprets the same bytes under another dtype, with no conversion at all.

Why is 0.1 + 0.2 not equal to 0.3? Binary floating point cannot store 0.1 or 0.2 exactly. Compare with np.isclose or np.allclose instead of ==.

Should I use float32 or float64? float64 is the default and the safe choice. float32 halves memory and is common in deep learning, but it carries only about seven decimal digits.

Why does mixing int and float give float? NumPy promotes to the smallest type that can hold both without losing the range. An int and a float therefore produce a float.

Choosing, summarised

Floats. float64 unless memory matters. float32 when it does and seven digits suffice, building up in float64.

ints. The default platform int is fine for indices and counts. Narrow on purpose for large arrays of small values, with headroom for the data you might see rather than the sample in front of you.

Booleans. bool_ for masks. One byte each, and they promote to ints so sum counts.

Strings. Fixed width, silently truncating. Check the width before assigning, or use pandas.

Objects. Almost always a sign the data doesn't want to be an array.

And in all cases: set it at creation. astype allocates a second full array. Getting it right the first time avoids both that copy and the class of bugs where an int array quietly refuses the float you assigned into it.

Check yourself

0 of 4

Answer without scrolling back up.

  1. What does a NumPy int8 do at 127 + 1?

  2. What dtype results from `int64 + float32`?

  3. `ints = np.arange(5); ints[0] = 9.99`. What is stored?

  4. Summing a million float32 ones does not give a million. Why?

Cheat sheet

Dtypes

Set and inspect the dtype of a NumPy array. See how integer overflow, float precision and type promotion affect calculations without warning.

NUMPY · vizlearn.in/numpy/dtypes.html

About the author

Ashish Jangra builds and maintains VizLearn. Every module here is written and the visualisation behind it hand-built, so the numbers in a readout come from the same code that draws the picture. Corrections are genuinely welcome and get priority over everything else — if a page states something wrong, or an animation misrepresents what the algorithm does, get in touch.