Skip to content

How to store numpy numbers

Numbers that come out of numpy, such as the largest of a row of scans or a reading from a sensor driver, go into a box without converting them first. An int, float, complex or bool field takes numpy's scalars, and a field can also declare one of numpy's fixed-width types, so it takes exactly the bytes of that width. This guide shows both, and what happens when a number doesn't fit.

1. Write numpy numbers to number fields

You assign a numpy scalar to an int or float field as you would a Python number, here one taken out of an array:

import numpy as np

from sharedbox import SharedBox


class Counter(SharedBox, name="example-counter"):
    parcels: int = 0
    weight: float = 0.0


counter = Counter()
scans = np.array([3, 7, 5], np.uint16)
weights = np.array([0.5, 1.5], np.float32)
counter.parcels = scans.max()
print(repr(counter.parcels))  # 7
counter.weight = weights[1]
print(repr(counter.weight))  # 1.5

The field reads back the type its annotation names, whatever type you wrote, so parcels reads back the int 7, not a numpy.uint16. An int field takes any integer numpy makes, a float field any real number, and a complex field any complex number. A bool field takes numpy.bool_, but neither an int nor a float field takes a bool.

2. Declare a field of a fixed-width type

When the width matters, for a C program that reads the box or for a large record, annotate the field with the numpy type itself:

class Scale(SharedBox, name="example-scale"):
    grams: np.uint16 = np.uint16(0)
    reading: np.float32 = np.float32(0.0)
    raw: np.int64 = np.int64(0)


scale = Scale()
scale.grams = np.uint16(1200)
print(repr(scale.grams))  # np.uint16(1200)
scale.reading = np.float32(0.1)
print(repr(scale.reading))  # np.float32(0.1)
print(repr(scale.raw))  # np.int64(0)

grams takes 2 bytes and reading takes 4, and each reads back as its numpy type. You can declare the integers int8, int16, int32, int64, uint8, uint16, uint32 and uint64, the floats float16, float32 and float64, complex64 and complex128, bool_, and datetime64, which section 4 covers.

A Python number works too: scale.grams = 1200 stores numpy.uint16(1200), and a float32 field rounds a Python float to the nearest float32, as numpy does. A type checker such as mypy checks each write against the annotation, so it reports scale.grams = 1200 as an error. A write of the declared type, as in the script, or of a value taken out of an array passes the check.

raw is a numpy.int64, which stores the same bytes as an int field. So do numpy.float64 and float, numpy.complex128 and complex, and numpy.bool_ and bool. Two classes with the same identity, one declaring int and the other numpy.int64, attach to the same box, and each reads back its own type.

3. Handle a number that doesn't fit

A fixed-width field refuses a number its width can't hold:

totals = np.array([70000], np.uint32)
try:
    scale.grams = totals[0]
except OverflowError as error:
    # Scale.grams holds an unsigned 16-bit integer; the value does not fit
    print(error)
samples = np.array([1e300])
try:
    scale.reading = samples[0]
except OverflowError as error:
    # Scale.reading holds a 32-bit float; the value does not fit
    print(error)
print(repr(scale.grams))  # np.uint16(1200)

If you assign one, you get an OverflowError and the field keeps its old value. A uint16 field refuses 70000 and -1. A float32 or float16 field refuses a finite number past its range, such as 1e300, rather than storing an infinity as numpy does. NaN and the infinities themselves are stored as they are. An int field refuses numpy.uint64(2**63), as it refuses any integer past 64 signed bits.

4. Store a time

A numpy.datetime64 field stores a count of nanoseconds since 1970, which covers the years 1678 to 2262:

class Delivery(SharedBox, name="example-delivery"):
    left_at: np.datetime64 = np.datetime64("NaT")


delivery = Delivery()
delivery.left_at = np.datetime64("2026-10-24T09:30", "m")
print(repr(delivery.left_at))  # np.datetime64('2026-10-24T09:30:00.000000000')

It takes a datetime64 of any unit and reads back one in nanoseconds, so the time you wrote in minutes comes back with nine digits after the seconds. A time before 1678 or after 2262 raises an OverflowError. NaT, numpy's value for no time, is stored and reads back as NaT.

5. Write numpy numbers to a union

A union keeps a numpy number in its own kind of number:

class Envelope(SharedBox, name="example-envelope"):
    stamp: float | int = 0


envelope = Envelope()
counts = np.array([5], np.int64)
envelope.stamp = counts[0]
print(repr(envelope.stamp))  # 5
envelope.stamp = weights[0]
print(repr(envelope.stamp))  # 0.5

The numpy.int64 5 matches neither member's class, so it goes to the first integer member, int, and reads back as 5; the numpy.float32 0.5 goes to the first real member, float. If that member can't hold the value, you get an OverflowError, and the box doesn't try the other members. Field types explains how a union picks its member for other values.

The whole script
"""The script of the guide "How to store numpy numbers"."""

import numpy as np

from sharedbox import SharedBox


class Counter(SharedBox, name="example-counter"):
    parcels: int = 0
    weight: float = 0.0


counter = Counter()
scans = np.array([3, 7, 5], np.uint16)
weights = np.array([0.5, 1.5], np.float32)
counter.parcels = scans.max()
print(repr(counter.parcels))  # 7
counter.weight = weights[1]
print(repr(counter.weight))  # 1.5


class Scale(SharedBox, name="example-scale"):
    grams: np.uint16 = np.uint16(0)
    reading: np.float32 = np.float32(0.0)
    raw: np.int64 = np.int64(0)


scale = Scale()
scale.grams = np.uint16(1200)
print(repr(scale.grams))  # np.uint16(1200)
scale.reading = np.float32(0.1)
print(repr(scale.reading))  # np.float32(0.1)
print(repr(scale.raw))  # np.int64(0)

totals = np.array([70000], np.uint32)
try:
    scale.grams = totals[0]
except OverflowError as error:
    # Scale.grams holds an unsigned 16-bit integer; the value does not fit
    print(error)
samples = np.array([1e300])
try:
    scale.reading = samples[0]
except OverflowError as error:
    # Scale.reading holds a 32-bit float; the value does not fit
    print(error)
print(repr(scale.grams))  # np.uint16(1200)


class Delivery(SharedBox, name="example-delivery"):
    left_at: np.datetime64 = np.datetime64("NaT")


delivery = Delivery()
delivery.left_at = np.datetime64("2026-10-24T09:30", "m")
print(repr(delivery.left_at))  # np.datetime64('2026-10-24T09:30:00.000000000')


class Envelope(SharedBox, name="example-envelope"):
    stamp: float | int = 0


envelope = Envelope()
counts = np.array([5], np.int64)
envelope.stamp = counts[0]
print(repr(envelope.stamp))  # 5
envelope.stamp = weights[0]
print(repr(envelope.stamp))  # 0.5

for box in (counter, scale, delivery, envelope):
    box.close()
    type(box).unlink()