# Read and write FITS files with astropy.io.fits without the classic mistakes

Troubleshoot astropy.io.fits file I/O: empty primary HDU (inspect with fits.info, read later HDUs or use getdata/getheader/getval), edits lost without mode='update'+flush(), OSError on writeto without overwrite=True, non-standard files via ignore_missing_simple/ignore_missing_end, memmap for large files, transparent .fits.gz/.fits.fz handling and CompImageHDU writes, case-insensitive header keyword updates.

Exact reference: {"kind":"skill_version","skill_id":"skl_R-52oLkfA4uwM5EWOBWilw","version_id":"skv_4phRQbtUeG7DhpOIU4fi1w"}

Applicability: [{"constraint":">=4.0","technology":"astropy","version_scheme":"semver"}]

# Read and write FITS files with astropy.io.fits without the classic mistakes

Use this when `hdul[0].data` is `None`, edits do not persist, a write fails
with "file already exists", or a FITS file will not open at all. (For WCS
and pixel-index conventions, see the openastronomy-coords skill — this one
is about the file I/O itself.)

## 1. Inspect the structure first — never assume HDU 0 holds the data

```python
from astropy.io import fits

fits.info('image.fits')
# Filename: image.fits
# No. Name      Type       Cards Dimensions Format
# 0   PRIMARY   PrimaryHDU    72 ()         --
# 1   SCI       ImageHDU      61 (400, 400) Int16
```

An empty `PRIMARY` (Dimensions `()`) is normal — many archives put the
science array in extension 1 or later. `hdul[0].data is None` is not a
corrupt file; read `hdul[1].data` or `hdul['SCI'].data`. Extensions can be
addressed by index, by name, or by `(EXTNAME, EXTVER)` tuple.

One-shot reads without managing an HDUList:

```python
data = fits.getdata('image.fits', ext=1)
hdr = fits.getheader('image.fits', ext=1)
data, hdr = fits.getdata('image.fits', ext=1, header=True)
flt = fits.getval('image.fits', 'FILTER', ext=0)   # single keyword value
```

## 2. Classify the failure

- **`hdul[0].data` is `None`** → header-only primary; the data lives in a
  later HDU (section 1). Not a corrupt file.
- **Edits vanish after close** → you opened in the default `readonly`
  mode. For in-place edits use `mode='update'` plus `flush()` (or let the
  context manager close the file); for a new file use `hdul.writeto(...)`
  (section 3).
- **`OSError: File 'out.fits' already exists`** → `writeto` refuses to
  clobber; pass `overwrite=True`.
- **`OSError` / verification errors on open** → non-standard or truncated
  file: retry with `ignore_missing_simple=True, ignore_missing_end=True`.
- **MemoryError / sluggish on huge files** → `memmap=True` (the default
  when it can work) and slice the array; only the slice is read.
- **`.fits.gz` / `.fits.fz` will not open** → they should: astropy opens
  gzip- and fpack-compressed FITS transparently. If you need to *write*
  tiled compression, build a
  `CompImageHDU(data=arr, compression_type='RICE_1')`.

## 3. The write patterns that actually persist

```python
# New file (or clobber an existing one):
fits.writeto('out.fits', data, hdr, overwrite=True)

# In-place edit of an existing file:
with fits.open('image.fits', mode='update') as hdul:
    hdul[1].header['EXPOSURE'] = (1200.0, 'total exposure, s')  # value + comment
    hdul[1].data *= 2.0
    hdul.flush()
```

Header keywords are case-insensitive; assigning `hdr['KEY'] = value` adds
or replaces the card, and `hdr['KEY'] = (value, 'comment')` attaches a
comment. Without `mode='update'`, the `with` block still closes the file —
your edits just never reach disk.

Appending or replacing a single extension of an existing file:

```python
fits.append('out.fits', new_data, new_hdr)        # adds a new HDU at the end
fits.update('out.fits', new_data, new_hdr, 'SCI')  # replaces the SCI extension
```

## 4. Tables and large files

```python
from astropy.table import Table

t = Table.read('catalog.fits', hdu=1)   # binary table -> Table, units kept from TUNITn
```

`Table.read` is usually nicer than `hdul[1].data` (a raw FITS_rec) for
catalog work. For images, remember `hdul[i].data` auto-applies the
`BZERO`/`BSCALE` scaling — pass `do_not_scale_image_data=True` to
`fits.open` if you want the raw stored values.

## 5. Checklist

1. `fits.info()` first — confirm which HDU holds the data.
2. Reading → `fits.open` in a `with` block, or `getdata`/`getheader`/
   `getval` for one-shots.
3. Editing in place → `mode='update'` + `flush()`; new file →
   `writeto(..., overwrite=True)`.
4. Broken file → `ignore_missing_simple` / `ignore_missing_end`.
5. Big file → `memmap=True`, slice before computing.
6. Compression → read transparently; write with `CompImageHDU` or `.fits.gz`.


## Supporting basis and limitations

Built from the astropy io.fits documentation (getdata/getheader/getval convenience functions, writeto/append/update, info), the STScI HST handbook chapter on working with FITS data in Python (mode='update' requirement for persisting edits), and community troubleshooting references for memmap, overwrite, and ignore_missing_simple/ignore_missing_end.

## Change and rationale

New skill: read and write FITS files with astropy.io.fits without the classic mistakes.

FITS I/O questions (empty hdul[0].data, edits not persisting, file-exists errors) are perennial astropy support traffic, and each has a different one-line fix that is only obvious after classifying the symptom. This skill gives agents the classify-first procedure so they stop re-reading corrupt-looking files that are actually fine.
