Skip to main content
NumPy · lesson 9 of 13
NumPy intermediate 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.

Frequently Asked Questions

How is a structured array different from a regular NumPy array?
A regular array holds one dtype for every element (all int64, all float64). A structured array's dtype is a list of named (field, subtype) pairs, so each element is a fixed-layout record — like a row in a table or a C struct. You index by field name (arr['age']) as well as by position (arr[0]).
When should I use a structured array instead of pandas?
Structured arrays are the right tool when you need a fixed, typed binary record layout — reading/writing a known binary format, interfacing with C code, or memory-mapping a large file of fixed-size records without loading it all into RAM. For general tabular analysis (joins, group-by, missing data), pandas is built on top of NumPy and is the better everyday tool.
Does field access on a structured array copy data?
Accessing a single field (arr['age']) returns a view into the original array's memory, not a copy — mutating it mutates the original. Accessing a list of fields (arr[['name', 'age']]) also returns a view in current NumPy versions. Call .copy() explicitly if you need an independent array.
Can a structured array's fields have different types, like a string and a float in the same row?
Yes — that's the whole point. Each field in the dtype gets its own type (a fixed-width string, an integer, a float, even a nested sub-array), and NumPy lays every record out with the same fixed byte size, which is what makes reading a binary file of records directly into an array possible.
What does the field order 'f0', 'f1' mean if I don't name my fields?
If you build a dtype from a string like np.dtype('i8, f4, S3') without names, NumPy auto-names the fields f0, f1, f2 in order. It's almost always clearer to name them explicitly with the list-of-tuples form: np.dtype([('id', 'i8'), ('score', 'f4')]).