NumPy uint8 (np.uint8) in Python: Range, Conversion and Overflow

np.uint8 is NumPy’s unsigned 8-bit integer data type: every value uses one byte and must be a whole number from 0 to 255. That makes it the standard type for image pixels and other compact data. Create a uint8 array with np.array(values, dtype=np.uint8) or convert one with arr.astype(np.uint8), but watch out: values outside 0–255 wrap around instead of raising an error. This guide covers the uint8 range, creating and converting arrays, overflow in arithmetic (and how to avoid it), floats and NaN, images, and a table of all NumPy integer and float data types.

All examples were run with Python 3.12.5 and NumPy 2.5.3 in the Windows Command Prompt. Reference: Data types and numpy.iinfo in the NumPy documentation.

What is np.uint8?

“u” means unsigned (no negative numbers), and “int8” means an 8-bit integer. Eight bits give 28 = 256 possible values, so a uint8 holds 0 to 255. np.iinfo() reports the exact limits:

import numpy as np

pixels = np.array([0, 128, 255], dtype=np.uint8)

print(pixels, pixels.dtype)
print("bytes per value:", pixels.itemsize)
info = np.iinfo(np.uint8)
print("range:", info.min, "to", info.max)

Output:

[  0 128 255] uint8
bytes per value: 1
range: 0 to 255
Command Prompt output of a NumPy uint8 array with its dtype, 1 byte per value and the range 0 to 255 from np.iinfo
One byte per value, range 0 to 255.

Create a uint8 array

Pass dtype=np.uint8 (or the string "uint8") to any array-creation function:

import numpy as np

a = np.array([10, 20, 30], dtype=np.uint8)       # from a list
b = np.zeros((2, 3), dtype=np.uint8)             # all zeros
c = np.full(4, 255, dtype="uint8")               # the string name works too
d = np.arange(0, 256, 64, dtype=np.uint8)

for name, arr in [("a", a), ("b", b), ("c", c), ("d", d)]:
    print(name, arr.dtype, arr.tolist())

Output:

a uint8 [10, 20, 30]
b uint8 [[0, 0, 0], [0, 0, 0]]
c uint8 [255, 255, 255, 255]
d uint8 [0, 64, 128, 192]

Convert an array to uint8 with astype()

astype(np.uint8) converts an existing array, and this is where most bugs start. Decimals are truncated, and integers outside 0–255 wrap around modulo 256 without any error. Only a Python integer that does not fit raises OverflowError:

import numpy as np

floats = np.array([0.0, 1.9, 127.5, 254.7])
print(floats.astype(np.uint8))                    # decimals are cut off, not rounded

ints = np.array([100, 255, 256, 300, -1])
print(ints.astype(np.uint8))                      # out of range: wraps around modulo 256

try:
    np.array([256], dtype=np.uint8)               # a Python int out of range is an error
except OverflowError as e:
    print("OverflowError:", e)

safe = np.clip(np.round(np.array([-20.4, 99.6, 300.2])), 0, 255).astype(np.uint8)
print(safe)                                       # round + clip first: the safe way

Output:

[  0   1 127 254]
[100 255   0  44 255]
OverflowError: Python integer 256 out of bounds for uint8
[  0 100 255]
Command Prompt output of NumPy astype uint8 truncating floats, wrapping 256 and 300 and -1 around, an OverflowError for 256 and a safe round and clip conversion
Truncation, wraparound, an OverflowError, and the safe round + clip pattern.

For values that may be outside the range, always use np.clip(np.round(x), 0, 255).astype(np.uint8). Converting floats that are out of range (like 300.0 or -1.0) directly is undefined behavior in NumPy, so the result can differ between computers.

uint8 overflow: why 250 + 10 = 4

Arithmetic on two uint8 arrays stays uint8, so results wrap around too. A common trap is taking the absolute difference of two images: the subtraction wraps before np.abs() ever sees a negative number. Convert to a wider type first:

import numpy as np

a = np.array([250, 10], dtype=np.uint8)
b = np.array([10, 20], dtype=np.uint8)

print("a + b         :", a + b)                  # 260 wraps to 4
print("a - b         :", a - b)                  # 10 - 20 wraps to 246
print("np.abs(a - b) :", np.abs(a - b))          # abs does not help: the wrap already happened
wide = a.astype(np.int16) - b.astype(np.int16)
print("int16 math    :", wide)
print("abs difference:", np.abs(wide).astype(np.uint8))

img = np.array([200, 240], dtype=np.uint8)
print("brighten +60  :", img + 60, "(wrapped)")
print("clipped       :", np.clip(img.astype(np.int16) + 60, 0, 255).astype(np.uint8))

Output:

a + b         : [ 4 30]
a - b         : [240 246]
np.abs(a - b) : [240 246]
int16 math    : [240 -10]
abs difference: [240  10]
brighten +60  : [ 4 44] (wrapped)
clipped       : [255 255]
Command Prompt output of NumPy uint8 overflow: 250 plus 10 gives 4, 10 minus 20 gives 246, np.abs does not help, and int16 math with clipping gives correct results
Wraparound in addition and subtraction, and the int16 + clip fix.

np.uint8 scalars

A single np.uint8 value behaves like the arrays: math with a Python int stays uint8 (with an overflow warning), and a float promotes the result to float64. Since NumPy 2, a Python int that does not fit in uint8 at all raises OverflowError. A scalar is also not iterable, which is where the “‘numpy.uint8’ object is not iterable” error comes from:

import numpy as np
import warnings

x = np.uint8(200)
print(repr(x), type(x).__name__)

with warnings.catch_warnings(record=True) as caught:
    warnings.simplefilter("always")
    print(x + 100)                               # stays uint8: 300 wraps to 44
print("warning:", caught[0].message)

try:
    x + 300                                      # 300 does not fit in uint8 at all
except OverflowError as e:
    print("OverflowError:", e)

print(type(x + 1.5).__name__, x + 1.5)           # a float promotes to float64
print(int(x) + 100)                              # convert to a Python int for normal math

try:
    for value in np.uint8(5):
        pass
except TypeError as e:
    print("TypeError:", e)                       # a scalar is not an array

Output:

np.uint8(200) uint8
44
warning: overflow encountered in scalar add
OverflowError: Python integer 300 out of bounds for uint8
float64 201.5
300
TypeError: 'numpy.uint8' object is not iterable

Convert floats to uint8 (0–1 and 0–255 data)

Image data is often stored as floats from 0.0 to 1.0. Multiply by 255 and round before converting, or 0.5 becomes 127 instead of 128. NaN has no integer value: NumPy warns and the result is undefined (it happened to be 0 on this computer, but you cannot rely on that), so replace NaNs yourself first with np.nan_to_num():

import numpy as np
import warnings

gray = np.array([0.0, 0.25, 0.5, 1.0])          # image data scaled 0..1
print((gray * 255).astype(np.uint8))             # 0.5 * 255 = 127.5 -> 127
print(np.round(gray * 255).astype(np.uint8))     # rounded: 128

with_nan = np.array([0.4, np.nan])
with warnings.catch_warnings(record=True) as caught:
    warnings.simplefilter("always")
    print((with_nan * 255).astype(np.uint8))     # NaN has no integer value
print("warning:", caught[0].message)
print(np.nan_to_num(with_nan * 255).astype(np.uint8))   # replace NaN with 0 first

Output:

[  0  63 127 255]
[  0  64 128 255]
[102   0]
warning: invalid value encountered in cast
[102   0]

Why uint8 is used for images

An RGB image is an array of shape (height, width, 3) with one uint8 per color channel. Using uint8 instead of float64 needs 8 times less memory:

import numpy as np

shape = (1080, 1920, 3)                          # one Full HD RGB image
for dtype in (np.uint8, np.int32, np.float32, np.float64):
    arr = np.zeros(shape, dtype=dtype)
    print(f"{np.dtype(dtype).name:<8} {arr.nbytes / 1024**2:8.1f} MB")

Output:

uint8         5.9 MB
int32        23.7 MB
float32      23.7 MB
float64      47.5 MB

Matplotlib, Pillow and OpenCV all expect uint8 images with values 0–255. This example builds an image directly from a uint8 array:

import numpy as np
import matplotlib.pyplot as plt

h, w = 200, 256
img = np.zeros((h, w, 3), dtype=np.uint8)
img[:, :, 0] = np.arange(w, dtype=np.uint8)       # red grows from left to right
img[:, :, 2] = 255 - np.arange(w, dtype=np.uint8) # blue fades out
img[80:120, 100:156] = [255, 255, 255]            # a white box

print(img.dtype, img.shape, img.min(), img.max())
plt.imshow(img)                                   # uint8 RGB is shown as 0..255 directly
plt.title("A uint8 RGB image: values 0 to 255")
plt.axis("off")
plt.show()

Output:

uint8 (200, 256, 3) 0 255
Matplotlib window showing an RGB image built from a NumPy uint8 array with a red and blue gradient and a white rectangle
A 200 × 256 image made from a uint8 array and shown with plt.imshow().

To save such an array as a picture, see save a NumPy array as a PNG with Matplotlib.

NumPy data types: uint8 compared with the others

NumPy has signed (int) and unsigned (uint) integers in 8, 16, 32 and 64 bits, plus 16-, 32- and 64-bit floats. The table below was printed by NumPy itself:

import numpy as np

print(f"{'dtype':<9}{'bytes':>6}  {'min':>27}  {'max':>27}")
for t in (np.int8, np.uint8, np.int16, np.uint16, np.int32, np.uint32, np.int64, np.uint64):
    i = np.iinfo(t)
    print(f"{np.dtype(t).name:<9}{np.dtype(t).itemsize:>6}  {i.min:>27,}  {i.max:>27,}")
for t in (np.float16, np.float32, np.float64):
    f = np.finfo(t)
    print(f"{np.dtype(t).name:<9}{np.dtype(t).itemsize:>6}  {'max ~' + format(float(f.max), '.3g'):>27}  precision {f.precision} digits")
print(np.array([1, 2]).dtype, np.array([1.0]).dtype, np.array([True]).dtype)   # defaults

Output:

dtype     bytes                          min                          max
int8          1                         -128                          127
uint8         1                            0                          255
int16         2                      -32,768                       32,767
uint16        2                            0                       65,535
int32         4               -2,147,483,648                2,147,483,647
uint32        4                            0                4,294,967,295
int64         8   -9,223,372,036,854,775,808    9,223,372,036,854,775,807
uint64        8                            0   18,446,744,073,709,551,615
float16       2                max ~6.55e+04  precision 3 digits
float32       4                 max ~3.4e+38  precision 6 digits
float64       8                max ~1.8e+308  precision 15 digits
int64 float64 bool
Command Prompt table of NumPy data types int8, uint8, int16, uint16, int32, uint32, int64, uint64 with bytes and ranges, and float16, float32, float64
Integer ranges from np.iinfo and float limits from np.finfo.
TypeRangeTypical use
np.uint80 to 255Images, bytes, small counters
np.int8-128 to 127Small signed values
np.int16 / np.uint16-32,768 to 32,767 / 0 to 65,535Audio samples, intermediate image math
np.int64about ±9.2 × 1018Default integer type
np.float32up to about 3.4 × 1038, 6 reliable digitsMachine learning, GPUs
np.float64up to about 1.8 × 10308, 15 reliable digitsDefault float type

If you do not pass a dtype, NumPy picks int64 for whole numbers and float64 for decimals (on Windows too since NumPy 2).

Continue with these NumPy tutorials:

Frequently asked questions

What is np.uint8 in Python?

NumPy’s unsigned 8-bit integer type. Each value takes one byte and must be between 0 and 255.

What is the range of uint8?

0 to 255. np.iinfo(np.uint8) returns min=0 and max=255.

How do I convert a NumPy array to uint8?

arr.astype(np.uint8). If the values may be outside 0–255 or have decimals, use np.clip(np.round(arr), 0, 255).astype(np.uint8).

Why does uint8 subtraction give large numbers like 246?

uint8 cannot be negative, so 10 – 20 wraps around to 246. Convert to np.int16 before subtracting.

What is the difference between uint8 and int8?

uint8 stores 0 to 255; int8 stores -128 to 127. Both use one byte.

Why do I get ‘numpy.uint8’ object is not iterable?

You are looping over a single value instead of an array. Wrap it with np.atleast_1d() or check where the scalar came from.