NumPy · lesson 9 of 13
NumPy Structured Arrays
Store heterogeneous, named-field records in one NumPy array — read them from CSV, round-trip to disk, and access fields by name.
By SkillByExample · Updated
Real-World Scenario
A sensor logs (name, age, weight) triples for lab animals into a CSV file. A colleague’s script
loads it as a plain float array and the animal names silently vanish — a plain NumPy array
can only hold one dtype for the whole array, and a string doesn’t fit next to a float. A
structured array fixes this: one array, one row per record, each field keeping its own type.
Defining a Structured Dtype
import numpy as np
dt = np.dtype([('name', 'U10'), ('age', 'i4'), ('weight', 'f8')])
pets = np.array(
[('Rex', 9, 81.0), ('Fido', 3, 27.5), ('Milo', 5, 42.3)],
dtype=dt,
)
print(pets)
print(pets['age'])
print(pets['age'].mean())
print(pets[1])
print(pets[1]['name'])
$ python structured_basics.py
[('Rex', 9, 81. ) ('Fido', 3, 27.5) ('Milo', 5, 42.3)]
[9 3 5]
5.666666666666667
('Fido', 3, 27.5)
Fido
dtype here is a list of (field name, field type) tuples — 'U10' is a Unicode string up to
10 characters, 'i4' a 4-byte int, 'f8' an 8-byte float. pets['age'] pulls out just that
field as a normal 1-D array you can call .mean() on directly. Indexing by row (pets[1])
returns a single record — a numpy.void scalar that itself supports field access.
Field Access Is a View, Not a Copy
import numpy as np
dt = np.dtype([('name', 'U10'), ('age', 'i4'), ('weight', 'f8')])
pets = np.array([('Rex', 9, 81.0), ('Fido', 3, 27.5)], dtype=dt)
ages = pets['age']
ages += 1
print(pets['age'])
print(ages.base is pets)
$ python structured_view.py
[10 4]
True
Mutating ages mutated pets — the same slicing-returns-a-view rule that applies to ordinary
NumPy arrays applies to a single field of a structured array too. ages.base is pets confirms
ages doesn’t own its memory; it’s a window into pets.
Reading Heterogeneous CSV Data
import numpy as np
import io
csv_text = """name,age,weight
Rex,9,81.0
Fido,3,27.5
Milo,5,42.3
"""
pets = np.genfromtxt(
io.StringIO(csv_text), delimiter=',', names=True,
dtype=[('name', 'U10'), ('age', 'i4'), ('weight', 'f8')],
)
print(pets.dtype.names)
print(pets)
print(pets[pets['weight'] > 30])
$ python structured_csv.py
('name', 'age', 'weight')
[('Rex', 9, 81. ) ('Fido', 3, 27.5) ('Milo', 5, 42.3)]
[('Rex', 9, 81. ) ('Milo', 5, 42.3)]
names=True tells genfromtxt to read field names from the header row and build a structured
dtype from them, one field per column — no manual parsing loop, and pets['weight'] > 30
boolean-indexes the whole record array at once, keeping every field of the matching rows
together.
Round-Tripping a Binary File
import numpy as np
dt = np.dtype([('name', 'U10'), ('age', 'i4'), ('weight', 'f8')])
pets = np.array([('Rex', 9, 81.0), ('Fido', 3, 27.5)], dtype=dt)
np.save('pets.npy', pets)
loaded = np.load('pets.npy')
print(loaded.dtype == pets.dtype)
print(loaded)
$ python structured_roundtrip.py
True
[('Rex', 9, 81. ) ('Fido', 3, 27.5)]
np.save writes the exact byte layout the dtype describes, field names and all — np.load
reconstructs an array with an identical dtype, not just identical values. This is the
structured-array use case that plain tabular tools don’t give you for free: a compact,
self-describing binary record format with no parsing step on the way back in.
Practice
1. Add a fourth field species (a string) to the pets dtype and filter for one species.
import numpy as np
dt = np.dtype([('name', 'U10'), ('species', 'U10'), ('age', 'i4'), ('weight', 'f8')])
pets = np.array(
[('Rex', 'dog', 9, 81.0), ('Fido', 'dog', 3, 27.5), ('Milo', 'cat', 5, 42.3)],
dtype=dt,
)
print(pets[pets['species'] == 'cat'])
$ python practice_species.py
[('Milo', 'cat', 5, 42.3)]
A boolean mask built from one field selects whole records, every field included.
2. Access two fields at once with pets[['name', 'weight']] and confirm it's a view.
import numpy as np
dt = np.dtype([('name', 'U10'), ('age', 'i4'), ('weight', 'f8')])
pets = np.array([('Rex', 9, 81.0), ('Fido', 3, 27.5)], dtype=dt)
subset = pets[['name', 'weight']]
print(subset)
print(subset.base is pets)
$ python practice_multifield.py
[('Rex', 81. ) ('Fido', 27.5)]
True
A list of field names returns a view that skips the unselected age bytes, so base is still
the original array.
3. Build a dtype using the unnamed string form np.dtype('i8, f4, U5') and print its auto-generated field names.
import numpy as np
dt = np.dtype('i8, f4, U5')
print(dt.names)
arr = np.array([(1, 2.5, 'ab')], dtype=dt)
print(arr['f0'], arr['f1'], arr['f2'])
$ python practice_unnamed.py
('f0', 'f1', 'f2')
[1] [2.5] ['ab']
Without explicit names NumPy numbers the fields f0, f1, f2 in order.
4. Sort the pets array by age descending and print the result.
import numpy as np
dt = np.dtype([('name', 'U10'), ('age', 'i4'), ('weight', 'f8')])
pets = np.array(
[('Rex', 9, 81.0), ('Fido', 3, 27.5), ('Milo', 5, 42.3)],
dtype=dt,
)
print(np.sort(pets, order='age')[::-1])
$ python practice_sort.py
[('Rex', 9, 81. ) ('Milo', 5, 42.3) ('Fido', 3, 27.5)]
order='age' sorts records by that field ascending; [::-1] reverses to descending.