Skip to content

    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:

    básico/cap07_tipos_dtype.pylinhas 10 a 15
    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)
    
    Output
    int64
    float64
    bool
    complex128
    
    TypeBytesRange or usage
    int8, int16, int32, int641, 2, 4, 8Signed integers. The int64 is the default
    uint8, uint16, uint32, uint641, 2, 4, 8Unsigned integers (the uint8 ranges from 0 to 255, like pixels)
    float32, float644, 8Decimals. The float64 is the default
    bool1True or false
    complex12816Complex 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:

    básico/cap07_tipos_dtype.pylinhas 20 a 22
    notas = np.array([7.9, 8.5, 9.99])
    print(notas.astype(int))
    print(np.round(notas).astype(int))
    
    Output
    [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:

    básico/cap07_tipos_dtype.pylinhas 27 a 28
    pequeno = np.array([250, 5], dtype=np.uint8)
    print(pequeno + 10)
    
    Output
    [ 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:

    básico/cap07_tipos_dtype.pylinhas 30 a 33
    try:
        np.array([300], dtype=np.uint8)
    except OverflowError as erro:
        print("OverflowError:", erro)
    
    Output
    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:

    básico/cap07_tipos_dtype.pylinhas 38 a 40
    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))
    
    Output
    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:

    básico/cap07_tipos_dtype.pylinhas 45 a 49
    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))
    
    Output
    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 in np.array, which converts everything to text (chapter 4). When a result seems strange, print the dtype first.

    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.