Chapter 7, Basic
Data types: dtype
Every element of an array has the same type, and this type determines the memory, the precision, and what happens when a number doesn't fit. Here lie the most expensive surprises of NumPy.
Code for this chapter: básico/cap07_tipos_dtype.py
The most common types
NumPy infers the type from the values, but you can (and often should) choose:
import numpy as np
print(np.array([1, 2, 3]).dtype)
print(np.array([1.0, 2, 3]).dtype)
print(np.array([True, False]).dtype)
print(np.array([1 + 2j]).dtype)
int64
float64
bool
complex128
| Type | Bytes | Range or usage |
|---|---|---|
int8, int16, int32, int64 | 1, 2, 4, 8 | Signed integers. The int64 is the default |
uint8, uint16, uint32, uint64 | 1, 2, 4, 8 | Unsigned integers (the uint8 ranges from 0 to 255, like pixels) |
float32, float64 | 4, 8 | Decimals. The float64 is the default |
bool | 1 | True or false |
complex128 | 16 | Complex numbers |
Convert with astype
The astype creates a new array with a different type. The conversion from decimal to integer truncates (discards the decimal part), instead of rounding:
notas = np.array([7.9, 8.5, 9.99])
print(notas.astype(int))
print(np.round(notas).astype(int))
[7 8 9]
[ 8 8 10]
The np.round uses "round to the nearest even" when the value is exactly in the middle: the 8.5 became 8. It's the same behavior as the round of Python.
When the number doesn't fit
Each type has a fixed range. Within a calculation, exceeding the limit wraps around (wraparound), without error and without warning. A uint8 that reaches 255 and receives 10 more wraps back to the beginning:
pequeno = np.array([250, 5], dtype=np.uint8)
print(pequeno + 10)
[ 4 15]
The 250 + 10 gave 4, and not 260. It is a silent error, and therefore one of the most dangerous: no warning, and the result seems plausible. Already in the creation, NumPy 2 protects you when the value is impossible:
try:
np.array([300], dtype=np.uint8)
except OverflowError as erro:
print("OverflowError:", erro)
OverflowError: Python integer 300 out of bounds for uint8
Decimal precision
Binary decimals do not exactly represent most fractions, and the float32 has even less precision than the float64. Equality comparison is a trap, and tolerance comparison is the remedy:
print(0.1 + 0.2 == 0.3, np.isclose(0.1 + 0.2, 0.3))
print(np.finfo(np.float64).eps)
print(np.float32(16_777_216) + np.float32(1))
False True
2.220446049250313e-16
1.6777216e+07
The eps is the smallest difference that the float64 distinguishes near 1. And the last result (1.6777216e+07, that is, 16,777,216) shows the limit of the float32: from 2²⁴ it can no longer represent all integers, and adding 1 to 16,777,216 returns the same number.
What happens when different types meet
When you combine types in a calculation, NumPy decides the type of the result by rules of promotion. NumPy 2 changed an important rule (NEP 50): a pure Python ("weak scalar") does not promote the type of the array, while a NumPy scalar promotes:
a = np.array([1, 2, 3], dtype=np.int8)
print((a + 1).dtype)
print((a + np.int64(1)).dtype)
print((np.float32(3) + 3.0).dtype)
print(np.result_type(np.int8, np.float32), np.result_type(np.int64, np.float32))
int8
int64
float32
float32 float64
The a + 1 continues int8, because the 1 is a Python integer and "adapts" to the array. But the a + np.int64(1) became int64. And the np.result_type responds, without calculating, what the type of the result would be.
Mixed data and small types
Two sources of silent bugs: using small types (
uint8,int16) in calculations that exceed the range, and mixing numbers with text innp.array, which converts everything to text (chapter 4). When a result seems strange, print thedtypefirst.
Exercise 1
Save memory with the smallest possible type
Write economizar(idades) that converts an array of ages (positive integers) to the smallest unsigned type that fits the largest value, using np.min_scalar_type.