File Handling & I/O

Fortran's I/O model is older than most filesystems and still one of its strengths: explicit file units, record-based reads, powerful formatted output, and a config-file format (NAMELIST) that nothing else matches. This lesson covers the open/write/read lifecycle, the three access modes, and how to make I/O failures part of the program instead of a crash.

Units and OPEN

Every file is attached to an unit number — a small integer your program chooses. Units 5 (stdin) and 6 (stdout) are preconnected. open connects a unit to a file name with a set of specifiers; close detaches it. The essential idiom reserves units with newunit= so you never collide with another open file or the preconnected ones:

program open_demo
  implicit none
  integer :: unit
  real :: x = 2.5
  open (newunit=unit, file="output.txt", status="replace", action="write")
  write (unit, '(a,f6.2)') "value = ", x
  close (unit)                  ! flush and release
end program open_demo

Key open specifiers: status="new" (must not exist), "replace" (overwrite), "old" (must exist), "scratch" (temp file, deleted on close); action="read|write|readwrite" declares the file's role; form="formatted|unformatted" selects text or binary; access="sequential|direct|stream" selects the access mode (default sequential). For historical Windows/Intel portability add convert="big_endian" where files cross architectures.

Formatted output: the FORMAT language

The (...) descriptors control every character of output. The four you will use daily: i0/iw integers (with or without fixed width), f.d fixed-point reals (f8.3), e.d scientific (e12.5), a text, plus 1x/nx spacing and / newlines. A format repeats to fit the data list, and a trailing i0 never truncates. Use a literal in the statement, or an explicit 100 format (...) label for complex layouts.

Reading data and robust error handling

read consumes records into variables. List-directed input (read (unit, *) a, b, c) splits on whitespace and skips blank lines, while a format gives byte-level control. Structured errors: append iostat=ios and iomsg=msg to any I/O statement; a nonzero ios signals failure and msg explains it. The end=/eor= labels handle end-of-file and record boundaries gracefully — a loop over an unknown-length file is the classic pattern:

program read_all
  implicit none
  integer :: unit, ios, n = 0
  real :: x
  character(len=128) :: msg
  open (newunit=unit, file="numbers.txt", status="old", action="read", &
        iostat=ios, iomsg=msg)
  if (ios /= 0) then
     print '(a)', trim(msg)      ! e.g. "No such file"
     stop 1
  end if
  do
     read (unit, *, iostat=ios) x   ! ios < 0 at end of file
     if (ios < 0) exit
     if (ios > 0) cycle             ! skip malformed records politely
     n = n + 1
  end do
  close (unit)
  print '(a,i0,a)', "read ", n, " numbers"
end program read_all

NAMELIST: config files without a parser

namelist /settings/ a, b, c declares a group of variables, then read (unit, nml=settings) reads a file of name = value lines — with one name per line, comments via !, and any order. The same statement writes the group back out self-describing. This is the configuration format of scientific Fortran; nothing ships a parser because Fortran ships the feature:

program config_demo
  implicit none
  integer :: unit
  integer :: iterations = 100
  real    :: tolerance = 1.0e-6
  logical :: verbose = .false.
  namelist /settings/ iterations, tolerance, verbose
  open (newunit=unit, file="run.nml", status="replace")
  write (unit, nml=settings)      ! writes the whole group, name = value
  close (unit)
  iterations = 0                   ! prove reading restores values
  open (newunit=unit, file="run.nml", status="old")
  read (unit, nml=settings)
  close (unit)
  print *, iterations, tolerance, verbose
end program config_demo

Access modes: sequential, direct, stream

Sequential (default): records are read in order — right for logs and CSVs. Direct (access="direct", recl=): every record is at a fixed, numbered position, readable in any order — right for structured grids and database-like files. Stream (access="stream"): the file is one long byte sequence with positional seeks via pos= — right for binary blocks and files shared with C. The choice is a design decision at open time, and changing it later means rewriting the read logic; pick based on whether your consumer reads front-to-back, by index, or by byte position.

Hygiene: never hard-code unit numbers for files you open yourself — use newunit= everywhere; close every unit you open; treat iostat as a mandatory part of every read/write that can fail.
program fmt
  implicit none
  real :: pi = 3.14159265358979
  integer :: year = 2026
  ! f8.4 = 8 wide, 4 decimals; 1x = one space; i0 = minimal integer
  print '(a,f8.4,1x,a,i0)', "pi ~", pi, "year", year
  print '(f12.6,e12.5)', pi, pi   ! fixed-point then scientific
end program fmt