NumPy·Lesson 3·12 min·0/3 exercises

dtypes, precision and casting

Fixed-width boxes — and every surprise that follows from them.

int8/16/32/64uint8float32/64overflow.astypepromotionnp.iinfonp.isclose

Watch it happen

Play it through, or step back and forth yourself.

int8
1 byte
int16
2 bytes
int32
4 bytes
int64
8 bytes

A dtype is a promise about how many bytes each element gets. An int8 gets one byte; an int64 gets eight. Same numbers, eight times the memory — and eight times the range.

The idea

Lesson 1 said an array has one dtype for all its elements. This lesson is about what that dtype actually is: a fixed-width box, and a rule for what the bits inside it mean.

Nearly every dtype surprise comes from the box being too small. Get comfortable with that one idea and the rest is bookkeeping.

Integers

int8, int16, int32, int64 — 1, 2, 4 and 8 bytes. A byte holds 256 patterns, so int8 spans -128 to 127. Drop the sign and uint8 spans 0 to 255, which is precisely why every image format uses it.

Never guess the limits — ask:

np.iinfo(np.int8)      # min=-128, max=127
np.iinfo(np.uint8)     # min=0,    max=255
np.finfo(np.float32)   # precision and range for floats
Hand-drawn notes showing integer dtypes as boxes of increasing width, each labelled with its range, and uint8 shown as the unsigned case.

The default int is platform-dependent

Worth knowing before it confuses you. np.arange(12).dtype is int64 on a normal 64-bit machine — but this lab runs on a 32-bit platform, so the plain int here is int32: four bytes, not eight.

That's why the lesson data is created with an explicit dtype=np.int64. It means the byte arithmetic in these lessons matches what you'll see on your own machine. If you ever depend on a specific width, spell it out — don't rely on the default.

Overflow is silent

This is the one to remember. Python integers grow as large as you like. NumPy integers do not — they wrap:

np.array([127], dtype=np.int8) + 1     # [-128]
np.array([250], dtype=np.uint8) + 10  # [4]
np.array([0], dtype=np.uint8) - 1     # [255]

No exception, no warning, just a wrong number. If you brighten a uint8 image by adding 60, every pixel above 195 wraps to near-black and your sunset grows holes. The fix is always the same shape: widen, compute, clip, narrow back.

np.clip(photo.astype(int) + 60, 0, 255).astype(np.uint8)

Two wrinkles worth knowing, both new in NumPy 2. A single NumPy scalar overflowing does raise a RuntimeWarning — but a whole array overflowing stays silent, and arrays are what you'll be working with. And writing an out-of-range value literally is now an error rather than a wrap:

np.array([300], dtype=np.uint8)     # OverflowError — caught at creation
np.array([300]).astype(np.uint8)   # [44] — casting still wraps
Hand-drawn notes showing uint8 overflow wrapping around a circular dial from 250 past 255 to 4, with no warning raised.

Floats are approximations

float64 — the default — carries roughly 16 significant digits; float32 about 7. Neither can store 0.1 exactly, because a tenth isn't a finite sum of halves any more than a third is a finite decimal.

So never compare floats with ==:

0.1 + 0.2 == 0.3               # False
np.isclose(0.1 + 0.2, 0.3)     # True
np.allclose(a, b)              # for whole arrays

float32 halves your memory and is often plenty — it's the default in most deep learning. Just know you've traded away precision to get it.

Hand-drawn notes showing that 0.1 plus 0.2 does not equal 0.3 exactly in floating point, and that np.isclose is the correct comparison.

Promotion

Combine two dtypes and NumPy picks the narrowest type that holds both, always widening: int8 + int64 → int64, int + float → float. It happens silently and is usually what you want. The exception worth watching is that dividing integers always gives you floats, even when it divides evenly.

Converting on purpose

.astype() is the explicit conversion, and it always copies. Going narrow truncates towards zero rather than rounding:

np.array([1.9, 2.9]).astype(int)          # [1 2]  — truncated
np.round([1.9, 2.9]).astype(int)         # [2 3]  — rounded first
np.array([300]).astype(np.uint8)         # [44]   — narrowing overflows too

And you can set the dtype at creation: np.array([1, 2], dtype=np.float32), np.zeros(5, dtype=int), np.arange(5, dtype="uint8"). Strings, booleans and dates all have dtypes too — bool is one byte per element, and True behaves as 1 in arithmetic, which is the trick behind counting with masks.

Practice

Write it yourself. The answer is there when you want it.

Putting the kettle on…

Starting up…

Write it yourself

not graded

Print the limits of np.int8 and np.uint8 with np.iinfo. Then make a uint8 array holding 250, add 10, and print the result — it wraps, silently. Print the safe version too: cast to int, add, np.clip to 0–255, cast back. Finish with np.array([1, 2, 3]) / 2 and look at what division did to the dtype.

Write something and press Run — the output appears here.

Your turn

3 exercises. Write the code yourself, then press Check — a nudge and the answer are there if you want them.

Cast np.array([300, 20, 30]) down to uint8. One of those values doesn't fit in a byte — run it and see what it becomes.

your answer

Convert np.array([1.9, 2.4, 3.7]) to integers by rounding — not truncating. You should get [2, 2, 4].

your answer

np.array([200, 250], dtype=np.uint8) plus 50, without wrapping. Values above 255 should stop at 255, and the result must still be uint8.

your answer