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()