# 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`.