Q&A library Python

Why does astype to a smaller integer type give the wrong number in NumPy?

Python Advanced dtypenumpyoverflow

Short answer

astype does not check the range, so a value too large for the target type wraps around silently. Check the range before downcasting, or use pandas to_numeric with downcast which picks a safe type.

The short version

astype converts without checking whether the values fit. A number too large for the target type wraps around, producing a silently wrong result with no error and no warning. Always check the range before downcasting, or let a tool choose the type for you.

The silent corruption

import numpy as np

np.array([300]).astype(np.int8)   # array([44], dtype=int8)

An int8 holds values from -128 to 127. The value 300 does not fit, so it wraps around to 44. There is no exception, no warning, just a number that is completely wrong and looks perfectly plausible.

Why this is so dangerous

Downcasting is a recommended memory optimisation, so people apply it to large frames as a matter of course. If even one value in a column exceeds the new type's range, that value is silently corrupted, and it will flow through every subsequent calculation looking like real data. This is the worst class of bug: no error, wrong answer.

Check the range first

col = df["count"]
print(col.min(), col.max())      # do these fit in the target type?

The limits of each type are available directly, so you can check programmatically.

info = np.iinfo(np.int8)
print(info.min, info.max)        # -128 127

safe = col.min() >= info.min and col.max() <= info.max

Only downcast when safe is True, and remember to allow headroom for values the data might contain in future, not just today.

Let pandas choose

pd.to_numeric with downcast picks the smallest type that actually fits the data, so it cannot overflow.

import pandas as pd
pd.to_numeric(df["count"], downcast="integer")

This is the safe way to get the memory saving, because the type is derived from the values rather than asserted by you.

Watch out for intermediate results too

Even when every stored value fits, an arithmetic result may not. Summing a long column of int8 values can overflow the accumulator.

small = np.array([100] * 1000, dtype=np.int8)
small.sum()                    # can overflow
small.sum(dtype=np.int64)      # accumulate in a wide type, safe

So the range check must cover not just the data but the calculations you intend to do on it.

Floats behave differently

Converting a float that is too large gives inf rather than wrapping, and converting a float to an integer truncates towards zero rather than rounding. Both are surprises worth knowing, and both argue for the same discipline: after any type conversion, look at the result and confirm it is what you expected, rather than assuming the conversion did the obvious thing.

Need this solved on your own data?

The library is free and always will be. When the problem is bigger than an answer, we take on the work.

Start a project