Quickstart

A five-minute tour of rustfits: open a file, read and slice an image, read and subset a table, write something back. Every code block below is self-contained — copy it into a script, swap the filename, and it runs.

Opening a file

rustfits.FITS is the file handle. Use it as a context manager so the file is closed and flushed when the block exits.

import rustfits

with rustfits.FITS("data.fits") as fits:
    print(fits)  # gives a pretty-printed list of HDUs
    for hdu in fits:
        # gives a pretty-printed view of the HDU
        print(hdu)

Each HDU is accessed by integer position (fits[0] is the primary HDU) or by EXTNAME (fits["sci"]; lookup is case- insensitive). HDUs come back typed: ImageHDU, TableHDU, CompressedImageHDU, etc.

In a repl, the FITS object gives a nice representation

>>> fits
file: /home/esheldon/data/tmp/tmp9fl85sgs/mix.fits
mode: r
extnum  hdutype     extname
0       IMAGE_HDU
1       BINARY_TBL  MYTABLE

As do the HDUs

>>> fits['image_hdu']
file: img.fits
extension: 0
type: IMAGE_HDU
image info:
  data type: f8
  dims: [300, 400]

>>> fits['table_hdu']
file: table.fits
extension: 1
type: BINARY_TBL
extname: WIDE
rows: 3
column info:
  index      i8
  flags      ?
  name       U   array[var]
  source     U12
  ra         f8
  dec        f8
  shape      f4  array[2]
  samples    f4  array[16]

File modes

rustfits.FITS accepts three modes:

Mode

Access

If the file exists

If the file is missing

"r" (default)

read-only

opens it

raises

"r+"

read + write

opens it (contents preserved)

raises

"w+"

read + write

truncates to zero length

creates it

Use "r+" to modify or append HDUs to an existing file without losing anything. Use "w+" when you want to start fresh — it is equivalent to fitsio’s "rw" plus clobber=True. "w+" is also the default for the top-level rustfits.write() convenience; pass mode="r+" to append to an existing file instead of truncating it.

Reading an image

Whole-array read:

arr = rustfits.read("image.fits")

with rustfits.FITS("image.fits") as fits:
    arr = fits[1].read()
    print(arr.shape, arr.dtype)

Slicing with __getitem__ (same axis convention as numpy — slowest axis first):

with rustfits.FITS("image.fits") as fits:
    stamp = fits[1][100:200, 50:150]   # 100x100 cutout
    row = fits[1][42, :]               # one row
    pixel = fits[1][10, 20]            # scalar

Only the tiles or strips that overlap the slice are read from disk — fine for grabbing cutouts out of a multi-GB image.

Writing pixels back

__setitem__ is symmetric with __getitem__: anything you can read this way you can write the same way. Open the file in "r+" (read-write):

import numpy as np

with rustfits.FITS("image.fits", "r+") as fits:
    fits[1][100:200, 50:150] = 0          # zero out a region
    fits[1][42, :] = np.arange(fits[1].shape[1])

A scalar RHS broadcasts across the selection; an ndarray must match the slice’s shape.

Reading a table

A TableHDU returns a numpy structured array:

with rustfits.FITS("catalog.fits") as fits:
    tab = fits[1].read()
    print(tab.dtype.names)
    print(tab["ra"][:5])

Read just some columns or some rows:

with rustfits.FITS("catalog.fits") as fits:
    hdu = fits[1]
    sub = hdu.read(columns=["ra", "dec"])      # column subset
    head = hdu.read(rows=slice(0, 100))        # first 100 rows
    picks = hdu.read(rows=[0, 5, 10, 17])      # fancy rows

Column subsets

Indexing a table with a column name returns a subset object — a lazy view that turns into an array (or smaller table) when sliced or read.

with rustfits.FITS("catalog.fits") as fits:
    ra_col = fits[1]["ra"]              # SingleColumnSubset
    ra_all = ra_col[:]                  # plain ndarray
    ra_first_100 = ra_col[:100]
    ra_picks = ra_col[[0, 5, 10]]

    sub = fits[1][["ra", "dec"]]        # ColumnSubset
    cat = sub[:]                        # structured ndarray
    first_10 = sub[:10]                 # smaller structured ndarray

Both subset types also expose .read() and .write() methods if you prefer named calls over slicing.

Writing into a table

The same indexing surface works for writes. hdu[i] = record replaces one row; hdu["col"] = arr replaces a whole column; subset objects support __setitem__ for patching individual cells or row ranges of a column:

import numpy as np

with rustfits.FITS("catalog.fits", "r+") as fits:
    hdu = fits[1]
    # Single-row write — value is a record (numpy.void) or a
    # length-1 structured array with the table's field names.
    hdu[0] = hdu[0]                                  # no-op
    # Whole-column write.
    hdu["flag"] = np.zeros(len(hdu), dtype="i4")
    # Single-cell write — symmetric with the read form
    # `hdu["ra"][5]`.
    hdu["ra"][5] = 123.4
    # Multiple rows of one column.
    hdu["ra"][[0, 1, 2]] = [10.0, 11.0, 12.0]

Append rows with append():

new_rows = np.zeros(3, dtype=hdu.dtype)
hdu.append(new_rows)

Creating a new file

The shortest path is rustfits.write(), which auto-detects image vs table from the value you pass it:

import numpy as np
import rustfits

img = np.arange(10000, dtype="f4").reshape(100, 100)
rustfits.write("out.fits", img)                          # → image HDU

cat = np.zeros(100, dtype=[("ra", "f8"), ("dec", "f8"), ("flag", "i4")])
rustfits.write("cat.fits", cat)                          # → table HDU

rustfits.write(
    "named.fits",
    {"x": np.arange(3), "y": np.arange(3) * 2.0},
    extname="sci",
)

rustfits.write() accepts only the universal kwargs (mode, extname, header). For type-specific knobs (compress=, quantize=, blank=, var_dtypes=, units=, bit_columns=, …), or for multiple HDUs in one file, open FITS directly and call write_image() / write_table():

with rustfits.FITS("out.fits", "w+") as fits:
    fits.write_image(img, extname="sci")
    fits.write_table(cat, extname="cat")

The "w+" mode truncates or creates; "r+" opens an existing file for read-write without truncating.

For code that processes HDUs without knowing their type ahead of time — copying HDUs from one file to another, for instance — write() is the method-form counterpart to rustfits.write() and auto-detects image vs table from its argument:

with rustfits.FITS("in.fits") as src:
    with rustfits.FITS("out.fits", "w+") as dst:
        for hdu in src:
            if hdu.has_data:
                dst.write(hdu.read())

Headers

Every HDU has a header attribute exposing the FITS cards as a dict-like view:

with rustfits.FITS("data.fits") as fits:
    hdr = fits[1].header
    exptime = hdr["exptime"]
    comment = hdr.comment_of("exptime")
    print(hdr.keys())

For “just the header, no data” workflows there’s a top-level rustfits.read_header() shortcut (default ext=0 picks the primary HDU):

hdr = rustfits.read_header("data.fits")           # primary
sci_hdr = rustfits.read_header("data.fits", ext="sci")

The returned FITSHeader outlives the file close — read-only access works after the function returns.

Mutation is straightforward in "r+" mode:

with rustfits.FITS("data.fits", "r+") as fits:
    fits[1].header["object"] = "M31"
    fits[1].header.add_comment("Processed by pipeline v2")

Walking the HDUs

Iterate to see what’s in a file:

with rustfits.FITS("data.fits") as fits:
    for i, hdu in enumerate(fits):
        print(i, hdu.extname, hdu.has_data)

Two HDU properties are useful for filtering without reading any data:

  • hdu.has_data — True iff the HDU has actual data (NAXIS > 0 and every NAXISn > 0). The primary HDU is often empty (header-only) and has_data is False there.

  • isinstance(hdu, ImageHDU) / TableHDU — pick by HDU type. CompressedImageHDU is a subclass of ImageHDU and CompressedTableHDU is a subclass of TableHDU, so an isinstance check on the base class matches both compressed and uncompressed.

A realistic “find the first image worth reading” pattern:

with rustfits.FITS("data.fits") as fits:
    for hdu in fits:
        if hdu.has_data and isinstance(hdu, rustfits.ImageHDU):
            img = hdu.read()
            break

The matching pattern for tables uses rustfits.TableHDU.

Where to next

  • Images — dtypes, scaling, BLANK masking, extend().

  • Tables — schema construction, VLA columns, schema edits.

  • Compression — tile-compressed images and tables, the Quantize config, the tile cache.

  • Headers — protected keys, CONTINUE chains, HIERARCH, FITSHeaderEdit for batched updates, checksums.