Input/Output: Units and Files

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Fortran 2023 (ISO/IEC 1539-1:2023, J3/23-007r1) and was checked on 2026-10-19. Every example was compiled warning-free with gfortran -std=f2023 -Wall -Wextra -pedantic -fimplicit-none -fcheck=all (GNU Fortran 16.2) and run, and the examples that are portable were also built and run with LLVM Flang 23. The output shown is real output; where the two compilers print something different, both outputs are shown and the difference is discussed. Examples that are deliberately wrong, or that use a feature one of the compilers rejects, are labelled as such and show the real diagnostics. Input/output is defined in clause 12 of the standard ("Input/output statements"): 12.2 gives the records, 12.3 the external files and access methods, 12.5 the connection of files to units including the open and close statements, 12.6 the data transfer statements, 12.8 the positioning statements, 12.9 flush, 12.10 inquire and 12.11 the error, end-of-record and end-of-file conditions. The edit descriptors that shape each record are in clause 13.

Fortran I/O is organised around three ideas that every other language folds together: a unit (a number, or a negative value chosen by the processor, that stands for an open file), a file (a sequence of records or of bytes) and the connection between the two, which carries properties such as the access method and the form. A program works with the unit; the open statement makes the connection. The running example, heatflow, uses this machinery in the module heatflow_io: a CSV file written with write_csv, a binary checkpoint written with write_stream and read back with read_stream, and the namelist configuration read by heatflow_config (see Internal Files, Namelist and Non-Advancing I/O). This page covers the model, the statements, the three access methods, positioning, error handling and the portability of binary files. The edit descriptors and format specifications are in Format Specifications and Edit Descriptors, internal files, namelist and non-advancing input and output in Internal Files, Namelist and Non-Advancing I/O, and user-defined derived-type input and output and asynchronous transfers in Derived-Type and Asynchronous I/O.

The I/O model: records, files and units

A record (12.2) is the unit of organisation of a file. The standard names three kinds:

Kind Content Written and read by

Formatted record

a sequence of characters, such as a line of text; its length is measured in characters and may be zero

formatted statements only (an explicit format, * or a namelist)

Unformatted record

a sequence of values in a processor-dependent form, with or without data; its length is measured in file storage units

unformatted statements only (no format)

Endfile record

a marker written by endfile, or implicitly when a file open for sequential access is closed, rewound or backspaced after a write; it can only be the last record

produces the end-of-file condition when read

There is no "physical record" in the language: a record is a logical notion, and a text file’s line ends or a binary file’s record markers are how a processor happens to store it. A file is either a record file, a sequence of records, or a stream file, a sequence of file storage units (12.3.5). A file storage unit is the basic unit of size and position for stream files, the unit of record length for unformatted files and the unit of file size for all external files. The intrinsic module constant file_storage_size (16.10.2.11) gives its width in bits, and the standard recommends an 8-bit octet, which both compilers use (the first example prints file_storage_size=8). The result of inquire (iolength=...), of recl= for unformatted files and of size= are all in these units, so they are bytes in practice. A processor may let one file be viewed as a record file and as a stream file; the relationship between the two views is processor dependent.

A file is external (12.3, a file on disk, a pipe, a terminal) or internal (12.4, a character variable that is used as a file). An external file is reached through a unit (12.5.1): an integer expression whose value is non-negative, or one of the constants input_unit, output_unit and error_unit of iso_fortran_env, or a value that newunit= returned, or the unit argument of an active defined input/output procedure, or the asterisk *. An internal unit is a character variable. Internal files get their own page; here a "unit" means an external one. A few rules shape everything else:

  • A unit is connected to at most one file at a time (12.5.4). Whether one file may be connected to two units is processor dependent (the last examples show the two compilers disagree).

  • Every statement except open, close, inquire and wait needs a unit that is connected. A close, inquire or wait may name a unit that does not exist.

  • The unit number identifies the same unit in all program units of the image (12.5.1), so read (6) in one procedure and rewind 6 in another refer to the same unit. Units are global in a way that variables are not.

  • Units are per image. On a parallel program with coarrays, input_unit is preconnected only on image 1 of the initial team; each image has its own output_unit and error_unit, and a file named the same on two images is not necessarily the same file (12.3.1). The processor may merge output from the images (see Coarrays, Images and Synchronization).

Preconnected units, print, read and write

Three units are connected before the program starts (12.5.5), all for sequential formatted access.

Unit Named constant Connected to Selected by *

standard input

input_unit (16.10.2.13)

preconnected for formatted input on image 1 only

read (*, ...), read fmt, list, and read without a control list

standard output

output_unit (16.10.2.24)

preconnected for formatted output

write (*, ...), print fmt, list

error output

error_unit (16.10.2.9)

processor-dependent unit for error reports, which may be the same as the output unit

none: write to error_unit explicitly

The values of the constants are processor dependent. Both compilers used for this page give input_unit=5, output_unit=6 and error_unit=0; that is a historic convention, and the only portable way to write to the error unit is the named constant. A program that writes write (6, ...) or write (0, ...) is relying on the convention, and GNU Fortran even lets the user move them with the environment variables GFORTRAN_STDIN_UNIT, GFORTRAN_STDOUT_UNIT and GFORTRAN_STDERR_UNIT. The statements themselves have three forms (12.6.1):

read (unit, fmt, iostat=ios) list      ! general form, with control specifiers
write (unit, fmt, iostat=ios) list
print fmt, list                        ! always unit *, always formatted
read fmt, list                         ! always unit *, always formatted (rare)

print has no unit and no control list, so it cannot report errors. For anything that matters (iostat=, non-advancing output, a unit that is not the terminal) use write. The next program shows the preconnected units, the three equivalent ways of writing to standard output, the properties of the preconnected units, the use of error_unit for diagnostics and a read from standard input that survives an empty input.

program preconnected_units
  use, intrinsic :: iso_fortran_env, only: input_unit, output_unit, error_unit, &
                                           file_storage_size, iostat_end, iostat_eor
  implicit none
  integer :: nx, ny, ios
  character(len=16) :: acc, frm, act

  print '(a, 3(i0, 1x))', 'input, output, error unit numbers: ', input_unit, output_unit, error_unit
  print '(a, i0, 2(a, i0))', 'file_storage_size=', file_storage_size, ' iostat_end=', iostat_end, &
    ' iostat_eor=', iostat_eor

  ! The three forms below all write to the same preconnected unit.
  write (output_unit, '(a)') 'write to output_unit'
  write (*, '(a)') 'write to *'
  print '(a)', 'print'

  ! Properties of the preconnected units.
  inquire (unit=output_unit, access=acc, form=frm, action=act)
  print '(4(a, 1x))', 'output_unit:', trim(acc), trim(frm), trim(act)
  inquire (unit=input_unit, access=acc, form=frm, action=act)
  print '(4(a, 1x))', 'input_unit: ', trim(acc), trim(frm), trim(act)

  ! Diagnostics belong on error_unit, so that they do not mix with results on stdout.
  write (error_unit, '(a)') 'a diagnostic message on error_unit'

  ! A bare READ and "read (*, ...)" both use the preconnected input unit.
  read (*, *, iostat=ios) nx, ny
  if (ios == 0) then
    print '(a, i0, a, i0)', 'read nx=', nx, ' ny=', ny
  else if (ios == iostat_end) then
    print '(a)', 'no input: end of file on standard input'
  else
    print '(a, i0)', 'read error, iostat=', ios
  end if
end program preconnected_units

Output with the standard output stream fed with 64 32 (the diagnostic on error_unit goes to the other stream and is not part of this output):

input, output, error unit numbers: 5 6 0
file_storage_size=8 iostat_end=-1 iostat_eor=-2
write to output_unit
write to *
print
output_unit: SEQUENTIAL FORMATTED WRITE
input_unit:  SEQUENTIAL FORMATTED READ
read nx=64 ny=32

Run again with empty standard input the last line becomes no input: end of file on standard input, which is the iostat_end branch. LLVM Flang 23 prints the same lines. When both streams were captured through one pipe in the test runs, the diagnostic came before the other output with GNU Fortran and after it with Flang, so the relative order of the two streams is not something to rely on. Source: J3/23-007r1: 12.5.1 Referring to a file, 12.5.5 Preconnection and 16.10.2 Intrinsic module ISO_FORTRAN_ENV, GNU Fortran manual: GFORTRAN_STDIN_UNIT

Two practical consequences. First, buffering is processor dependent: GNU Fortran buffers regular files and treats terminals and pipes as unbuffered, and other processors differ, so a program that must show progress at a known moment calls flush (output_unit) (see flush and the buffering of unit output). Second, error_unit is the right place for messages that must not be mixed with results that other programs parse, such as the CSV that heatflow writes.

Opening a file: the open statement

The open statement (12.5.6) connects a file to a unit, creates a file, or changes the modes of an existing connection. This is its form with every specifier of Fortran 2023 (R1205):

open (unit=u, file='name', status='old', action='read', access='sequential', form='formatted', &
      position='asis', recl=n, iostat=ios, iomsg=msg)
open (newunit=u, file='name', ...)             ! a free negative unit number is returned in u

The unit comes first or is given as unit=; with newunit= there is no unit. Each specifier may appear once (C1203). The values are case-insensitive and trailing blanks are ignored (12.5.6.2).

Specifier Values Default Notes

unit=

non-negative integer

none (or newunit=)

C1204 and C1205: exactly one of unit and newunit

newunit=

integer variable

none

gets a negative value, never -1 and never input_unit, output_unit or error_unit (12.5.6.13); needs file= or status='scratch'; F2008

file=

character expression

processor-dependent for a new unit

required with status='new' and 'replace', forbidden with 'scratch'; a unit that is not connected and has no file= needs status='scratch' (12.5.6.10)

status=

old, new, replace, scratch, unknown

unknown

old requires the file, new requires its absence, replace deletes and recreates it, scratch creates a file that goes away at close or at the end of the program (12.5.6.19)

action=

read, write, readwrite

processor-dependent

read forbids write, print and endfile; write forbids read

access=

sequential, direct, stream

sequential

12.5.6.3

form=

formatted, unformatted

formatted for sequential, unformatted for direct and stream

12.5.6.11

position=

asis, rewind, append

asis

only for sequential and stream; append positions before the endfile record or at the end (12.5.6.15)

recl=

positive integer

processor-dependent for sequential; required for direct

characters for formatted and file storage units for unformatted; not for stream (12.5.6.16)

iostat=, iomsg=, err=

integer variable, character variable, label

none

see Error handling: iostat, iomsg, err, end and eor

asynchronous=

yes, no

no

allows asynchronous transfers on the unit; see Derived-Type and Asynchronous I/O

encoding=

utf-8, default

default

formatted connections only; utf-8 makes the characters of ISO 10646 kind (12.5.6.9)

blank=

null, zero

null

how blanks in numeric input fields are read; changeable mode

decimal=

point, comma

point

decimal symbol; changeable mode

delim=

apostrophe, quote, none

none

delimiter of character values in list-directed and namelist output; changeable mode

pad=

yes, no

yes

whether short input records are padded with blanks; changeable mode

round=

up, down, zero, nearest, compatible, processor_defined

processor-dependent

rounding of formatted conversion; changeable mode

sign=

plus, suppress, processor_defined

processor_defined

sign of non-negative numbers on output; changeable mode

leading_zero=

print, suppress, processor_defined

processor_defined

optional zero before the decimal point; changeable mode; Fortran 2023

Three consequences of this table matter every day. The status= default is unknown, which in practice means "open if it exists, create it otherwise": a typing error in a file name then silently creates an empty file instead of failing, so read-only code should say status='old', action='read'. The action= default is processor dependent. GNU Fortran tries readwrite, then read, then write and fails only when all three do, which is why a read-only file that is opened without action= works but a later write to it fails at run time (this is documented in its manual, section "Files opened without an explicit ACTION= specifier"). And the changeable modes (blank, decimal, delim, pad, round, sign, leading_zero) can be changed without reconnecting by a second open on the same unit with no file= or with the same file, or for a single statement by the same keyword in read/write, or inside a format by a control edit descriptor (12.5.2).

The next program opens a file, queries the connection with inquire, closes it, queries the file by name, uses a scratch file and removes a file with close (status='delete').

program open_inquire_close
  implicit none
  integer :: u, v, ios, rl
  logical :: exists, opened
  character(len=256) :: msg
  character(len=32) :: acc, frm, act, pos, pad, dec, blank, rnd, sgn
  character(len=64) :: name

  ! NEWUNIT= picks a free negative unit number, so that no code has to reserve a number.
  open (newunit=u, file='demo.txt', status='replace', action='readwrite', form='formatted', &
        access='sequential', position='asis', iostat=ios, iomsg=msg)
  if (ios /= 0) error stop trim(msg)
  print '(a, l1)', 'newunit value is negative: ', u < 0

  write (u, '(a)') 'first line'
  write (u, '(a)') 'second line'

  ! Inquire by unit: properties of the connection.
  inquire (unit=u, opened=opened, name=name, access=acc, form=frm, action=act, position=pos, &
           pad=pad, decimal=dec, blank=blank, round=rnd, sign=sgn, recl=rl)
  print '(a, l1, 2a)', 'opened=', opened, ' name=', trim(name)
  print '(a)', 'access=' // trim(acc) // ' form=' // trim(frm) // ' action=' // trim(act) &
    // ' position=' // trim(pos)
  print '(a)', 'pad=' // trim(pad) // ' decimal=' // trim(dec) // ' blank=' // trim(blank) &
    // ' sign=' // trim(sgn)
  print '(a)', 'round=' // trim(rnd)
  print '(a, i0)', 'maximum record length recl=', rl

  close (u)

  ! Inquire by file: the file exists, but is no longer connected.
  inquire (file='demo.txt', exist=exists, opened=opened, number=v)
  print '(a, l1, a, l1, a, i0)', 'file exists=', exists, ' opened=', opened, ' number=', v

  ! A scratch file has no name and disappears at CLOSE or at normal termination.
  open (newunit=u, status='scratch', form='unformatted', access='stream')
  inquire (unit=u, named=opened)
  print '(a, l1)', 'scratch file is named: ', opened
  close (u)

  ! CLOSE with STATUS='DELETE' removes the file.
  open (newunit=u, file='demo.txt', status='old')
  close (u, status='delete')
  inquire (file='demo.txt', exist=exists)
  print '(a, l1)', 'after close(status=delete) exists=', exists
end program open_inquire_close

Output with GNU Fortran 16.2:

newunit value is negative: T
opened=T name=demo.txt
access=SEQUENTIAL form=FORMATTED action=READWRITE position=APPEND
pad=YES decimal=POINT blank=NULL sign=PROCESSOR_DEFINED
round=PROCESSOR_DEFINED
maximum record length recl=2147483647
file exists=T opened=F number=-1
scratch file is named: F
after close(status=delete) exists=F

Output with LLVM Flang 23:

newunit value is negative: T
opened=T name=demo.txt
access=SEQUENTIAL form=FORMATTED action=READWRITE position=ASIS
pad=YES decimal=POINT blank=NULL sign=SUPPRESS
round=NEAREST
maximum record length recl=2147483647
file exists=T opened=F number=-1
scratch file is named: F
after close(status=delete) exists=F

The differences are the point of the example. position reads APPEND with gfortran and ASIS with Flang after two writes; sign and round read PROCESSOR_DEFINED with gfortran and SUPPRESS and NEAREST with Flang, because the standard allows a processor to report either the keyword it was given or the mode it chose. Neither of these values may be relied on; what the standard fixes is the set of possible values (12.10.2), the upper case of returned strings and the UNDEFINED result when there is no connection. The maximum record length of a sequential formatted file is 2147483647 characters with both compilers. inquire by file after the close shows opened=F and number=-1 (12.10.2.19: -1 when no unit is connected to the file). Source: J3/23-007r1: 12.5.6 OPEN statement, 12.5.7 CLOSE statement and 12.10 File inquiry statement, Fortran Programming Language: Best Practices, File Input/Output

newunit and the old unit-number idioms

newunit= (introduced in Fortran 2008; the gfortran wiki lists it as "Finding a unit when opening a file") returns a negative value that no other part of the program can have chosen: a classic non-negative unit number is a global resource, and a library that opens unit 10 clashes with a main program that opened unit 10. In a test run the first two values were -10 and -11 for GNU Fortran 16.2 and -2 and -3 for Flang 23; the standard allows any negative value other than -1 and the three named units (12.5.6.13). Pass the value around like any other integer: it may be stored in a derived type, passed to a procedure, used in inquire and printed. Code from before Fortran 2008 reserves unit numbers by convention or searches for a free one with inquire (unit=n, opened=...); the next example does that, for reading old code, and shows how the other unit-related rules behave.

module unit_utils
  implicit none
  private
  public :: free_unit
contains
  ! The pre-Fortran 2008 idiom, still common in old code: search for a unit that is not in use.
  ! New code should use NEWUNIT= instead, which cannot clash with a unit number a library picked.
  function free_unit() result(n)
    integer :: n
    logical :: is_open
    integer :: ios
    do n = 10, 99
      inquire (unit=n, opened=is_open, iostat=ios)
      if (ios == 0 .and. .not. is_open) return
    end do
    n = -1
  end function free_unit
end module unit_utils

program unit_numbers
  use unit_utils, only: free_unit
  implicit none
  integer :: u, v, w, ios
  character(len=100) :: msg
  character(len=20) :: line

  msg = ''
  u = free_unit()
  open (u, file='a.txt', status='replace')
  v = free_unit()
  print '(a, i0, a, i0)', 'first free unit was ', u, ', next free unit is ', v

  open (newunit=w, status='scratch')
  print '(a, l1)', 'a newunit value never equals a classic unit number: ', w < 0

  ! Connecting the same unit to another file closes the first file implicitly.
  write (u, '(a)') 'in a.txt'
  open (u, file='b.txt', status='replace')
  write (u, '(a)') 'in b.txt'
  close (u)
  open (u, file='a.txt', status='old')
  read (u, '(a)') line
  print '(a)', 'a.txt still contains: ' // trim(line)
  close (u, status='delete')

  ! Connecting one file to two units is processor dependent.
  open (10, file='b.txt', status='old')
  open (11, file='b.txt', status='old', iostat=ios, iomsg=msg)
  print '(a, i0, a)', 'second unit on the same file: iostat=', ios, ' iomsg=[' // trim(msg) // ']'
  close (10, status='delete')
  close (11)
end program unit_numbers

With GNU Fortran 16.2:

first free unit was 10, next free unit is 11
a newunit value never equals a classic unit number: T
a.txt still contains: in a.txt
second unit on the same file: iostat=0 iomsg=[]

With LLVM Flang 23:

first free unit was 10, next free unit is 11
a newunit value never equals a classic unit number: T
a.txt still contains: in a.txt
second unit on the same file: iostat=1037 iomsg=[OPEN(UNIT=11,FILE='b.txt'): file is already connected to unit 10]

The first three lines agree. The standard says connecting an already-connected unit to a different file first closes the original file (12.5.6.1), which is why a.txt is intact after unit u moved to b.txt. Opening one file on two units is processor dependent (12.5.4): GNU Fortran 16.2 accepts it (iostat=0), Flang refuses it with iostat=1037 and the message that the file is already connected to unit 10. Input and output on two units connected to the same file give processor-dependent results, so do not depend on either behaviour.

Unit numbers also have a legacy trap: a unit that was never opened is not an error for most compilers.

! Writing to a unit that was never opened: not allowed by the standard, tolerated by compilers.
program implicit_open
  implicit none
  logical :: exists
  character(len=64) :: name
  write (23, '(a)') 'no OPEN statement for unit 23'
  inquire (unit=23, name=name)
  print '(a)', 'unit 23 is connected to: ' // trim(name)
  inquire (file='fort.23', exist=exists)
  print '(a, l1)', 'file fort.23 exists: ', exists
  close (23, status='delete')
end program implicit_open

GNU Fortran 16.2 and LLVM Flang 23 print the same two lines:

unit 23 is connected to: fort.23
file fort.23 exists: T

This is the traditional "fort.N" behaviour: a write to a unit that is not connected creates the file fort.23 and connects it. The standard says a unit that does not exist or is not connected shall not be referred to by a data transfer statement (12.5.3 and 12.5.4), so this is tolerated rather than specified. It turns a missing open or an off-by-one unit number into a stray fort.N file instead of an error, and -std=f2023 does not diagnose it. Source: J3/23-007r1: 12.5.3 Unit existence and 12.5.4 Connection of a file to a unit

close

close (u, status=..., iostat=..., iomsg=..., err=...) disconnects the unit (12.5.7). status= is keep or delete; keep is the default except for scratch files, which are always deleted, and keep is an error for a scratch file. A close on a unit that does not exist, or has no file, is permitted and does nothing. At normal termination every connected unit is closed as if by close without status= (12.5.7.1). Closing a file that was written to a sequential unit writes an endfile record implicitly (12.2.4). A close also performs a wait for any pending asynchronous transfer (12.5.7.1).

Querying a connection: inquire

inquire (12.10) has three forms, selected by which of unit=, file= or iolength= it contains (C1248 forbids unit= and file= together):

  • Inquire by unit asks about a connection and its file: inquire (unit=u, ...). The unit need not exist or be connected; then most answers are UNDEFINED or UNKNOWN.

  • Inquire by file asks about a named file, whether or not it is connected: inquire (file='x.dat', ...). This is the way to ask whether a file exists before opening it, but the answer is stale as soon as another process creates or deletes the file, so check iostat on the open as well.

  • Inquire by output list gives the length of unformatted records: inquire (iolength=n) list. It is the portable way to compute recl= for direct access (12.10.3).

The specifiers (R1231, 12.10.2) fall into groups:

Group Specifiers Meaning

Existence and connection

exist=, opened=, number=, named=, name=

does the file or unit exist, is it connected, the unit number connected to a file (-1 if none), whether it has a name and what the processor calls it

Properties of the connection

access=, action=, form=, position=, recl=, asynchronous=, encoding=

what open was given or the processor chose; UNDEFINED if not connected; recl= is -1 when not connected and -2 for stream access

Current modes

blank=, decimal=, delim=, pad=, round=, sign=, leading_zero=

the connection modes in effect (formatted connections only, else UNDEFINED)

What the file allows

sequential=, direct=, stream=, formatted=, unformatted=, read=, write=, readwrite=

YES, NO or UNKNOWN: the allowed access methods, forms and actions of the file

Positions and sizes

nextrec=, pos=, size=

the next record number for direct access, the next file storage unit for stream access, the file size in file storage units (-1 if unknown)

Asynchronous transfers

pending=, id=

is a transfer still running (see Derived-Type and Asynchronous I/O)

Error reporting

iostat=, iomsg=, err=

as for the other statements; iostat is iostat_inquire_internal_unit if the unit is internal

Returned character values are upper case, except name= (12.10.2.1). The size= of a file that can be connected for stream access is the number of the highest-numbered file storage unit, so for a binary file it is the exact byte count, as the stream example below shows; for a sequential or direct file the relation to the data is processor dependent, because of record markers.

Two things are different between the compilers and worth knowing. First, Fortran 2023’s leading_zero= specifier in inquire, open and write is not accepted by GNU Fortran 16.2 (see leading_zero: the Fortran 2023 specifier). Second, inquire (unit=...) of a unit that was opened with status='scratch' has no name, so named= is false and name= must not be used.

Choosing the access method and the form

Two independent choices describe how a file is used: the access method (sequential, direct, stream) and the form (formatted, unformatted). The diagram gives the usual decision; the sections that follow describe each combination.

flowchart TD A["What is the file for?"] --> B{"Text for people or other tools: CSV, logs, configuration?"} B -->|yes| C["access='sequential', form='formatted'
one record per line"] B -->|no| D{"Fixed-size records read and rewritten in any order?"} D -->|yes| E["access='direct', form='unformatted'
recl= from inquire iolength"] D -->|no| F{"Binary file that another program or language reads, or needs byte positions?"} F -->|yes| G["access='stream', form='unformatted'
no record markers, pos= positioning"] F -->|no| H["access='sequential', form='unformatted'
record markers, Fortran-to-Fortran only"]
Combination Records Positioning Use in heatflow

sequential, formatted

lines of text, any length up to recl=

rewind, backspace, endfile, position=

the CSV output and the configuration (write_csv)

sequential, unformatted

one record per statement, with record markers around the data

rewind, backspace, endfile, position=

intermediate files exchanged between Fortran programs

direct, unformatted

fixed length, numbered from 1 (rec=), also formatted

rec= on each statement

a table of rows read by number

direct, formatted

fixed length text records

rec= on each statement

rare; fixed-width databases

stream, unformatted

none; a sequence of file storage units

pos=, inquire (pos=), no backspace

the checkpoint (write_stream, read_stream)

stream, formatted

text with record markers (line ends) but addressable

pos= equal to 1 or a value from inquire (pos=)

none; for text that is read and written together with positions

The sections below follow the table. The flowchart’s last branch (sequential unformatted) is the oldest and most portable-looking binary format and the least portable in practice, for the reasons in Unformatted sequential files: record markers and portability.

Sequential formatted files: the CSV of heatflow

A sequential file is a sequence of records read or written in order (12.3.3.2). The module below holds the CSV writer that heatflow uses, plus a function that counts the rows of a connected file by reading to the end and a reader that stops at the first problem. Reading until iostat becomes non-zero is the standard loop for files of unknown length; the rewind after the loop restores the position for the next reader. Note that the module declares what it exports and that the I/O statements all carry iostat= so that a short or damaged file becomes a return value, not a crash.

module heatflow_csv_demo
  use, intrinsic :: iso_fortran_env, only: real64
  implicit none
  private
  public :: dp, write_csv, count_rows, read_csv
  integer, parameter :: dp = real64
contains
  subroutine write_csv(u, field)
    integer, intent(in) :: u
    real(dp), intent(in) :: field(:, :)
    integer :: j
    do j = 1, size(field, 2)
      write (u, '(*(es12.5, :, ","))') field(:, j)
    end do
  end subroutine write_csv

  ! Count the records of a connected sequential file by reading to end of file.
  function count_rows(u) result(n)
    integer, intent(in) :: u
    integer :: n, ios
    character(len=1) :: dummy
    n = 0
    rewind (u)
    do
      read (u, '(a)', iostat=ios) dummy
      if (ios /= 0) exit
      n = n + 1
    end do
    rewind (u)
  end function count_rows

  subroutine read_csv(u, field, ios, msg)
    integer, intent(in) :: u
    real(dp), intent(out) :: field(:, :)
    integer, intent(out) :: ios
    character(len=*), intent(out) :: msg
    integer :: j
    ios = 0
    msg = ''
    do j = 1, size(field, 2)
      read (u, *, iostat=ios, iomsg=msg) field(:, j)
      if (ios /= 0) return
    end do
  end subroutine read_csv
end module heatflow_csv_demo

program sequential_csv
  use heatflow_csv_demo
  implicit none
  real(dp) :: field(4, 3), back(4, 3), extra(4, 5)
  integer :: u, ios, i, j
  character(len=128) :: msg

  field = reshape([(real(i, dp) * 0.25_dp, i=1, 12)], [4, 3])

  open (newunit=u, file='field.csv', status='replace', action='readwrite')
  call write_csv(u, field)
  print '(a, i0)', 'rows written: ', count_rows(u)

  call read_csv(u, back, ios, msg)
  print '(a, i0, a, l1)', 'iostat=', ios, ' max difference < 1e-5: ', &
    maxval(abs(back - field)) < 1.0e-5_dp

  ! Ask for more rows than the file has: the end-of-file condition is reported, not fatal.
  rewind (u)
  call read_csv(u, extra, ios, msg)
  print '(a, i0, a, l1)', 'iostat=', ios, ' is_iostat_end: ', is_iostat_end(ios)
  print '(a)', 'iomsg: ' // trim(msg)

  ! Show the text of the file itself.
  rewind (u)
  do j = 1, 3
    read (u, '(a)') msg
    print '(a)', trim(msg)
  end do
  close (u, status='delete')
end program sequential_csv

Output with GNU Fortran 16.2:

rows written: 3
iostat=0 max difference < 1e-5: T
iostat=-1 is_iostat_end: T
iomsg: End of file
 2.50000E-01, 5.00000E-01, 7.50000E-01, 1.00000E+00
 1.25000E+00, 1.50000E+00, 1.75000E+00, 2.00000E+00
 2.25000E+00, 2.50000E+00, 2.75000E+00, 3.00000E+00

Output with LLVM Flang 23 (only the wording of the iomsg line differs):

rows written: 3
iostat=0 max difference < 1e-5: T
iostat=-1 is_iostat_end: T
iomsg: End of file during input
 2.50000E-01, 5.00000E-01, 7.50000E-01, 1.00000E+00
 1.25000E+00, 1.50000E+00, 1.75000E+00, 2.00000E+00
 2.25000E+00, 2.50000E+00, 2.75000E+00, 3.00000E+00

Details worth remembering from this example:

  • Each write statement writes one record and (*(es12.5, :, ",")) is a format with an unlimited repeat group and the colon edit descriptor that stops the output at the end of the list, so there is no trailing comma (the descriptors are in Format Specifications and Edit Descriptors).

  • read (u, *, ...) field(:, j) reads one value list in list-directed mode, where commas and blanks are both separators. It reads as many records as it needs to supply the list. A row that has too few values reads the start of the next row, so check iostat and validate sizes.

  • An end-of-file condition on a read with iostat= does not terminate the program (12.11.3): the variable is set to the negative constant iostat_end, the input list is undefined, and a file with an endfile record is positioned after it. The rewind that follows is required before the file is read again.

  • The messages in iomsg are not standardised; iostat values other than the end and end-of-record constants are processor-dependent positive numbers.

The is_iostat_end (ios) and is_iostat_eor (ios) intrinsic functions (16.9.116 and 16.9.117) test an iostat value without naming the constant, which is convenient when iostat_end would clash with a local name.

Direct access files

A direct access file is a collection of numbered records of the same length (12.3.3.3). The records are read and written in any order and need not be written in order of their numbers. recl= is required in the open, the record number is given by rec= in each read or write, and the record numbers start at 1. The file cannot be positioned with rewind, backspace or endfile (12.8.1), cannot be used with list-directed or namelist formatting and cannot be read with non-advancing transfers. The length of an unformatted record is best computed with the inquire-by-output-list form, which accounts for the processor’s representation of exactly the list that will be transferred.

program direct_access
  use, intrinsic :: iso_fortran_env, only: real64, int32, file_storage_size
  implicit none
  integer, parameter :: dp = real64
  integer, parameter :: nx = 5, ny = 4
  real(dp) :: field(nx, ny), row(nx)
  integer :: u, j, ios, reclen, next, sz
  character(len=100) :: msg

  field = reshape([(real(j, dp), j=1, nx*ny)], [nx, ny])

  ! IOLENGTH= gives the record length, in file storage units, for exactly this output list.
  inquire (iolength=reclen) row
  print '(a, i0, a, i0, a)', 'iolength of one row: ', reclen, ' file storage units (', &
    reclen * file_storage_size, ' bits)'

  ! Direct access requires RECL=; the file holds fixed-length, numbered records.
  open (newunit=u, file='rows.bin', status='replace', access='direct', form='unformatted', &
        recl=reclen, action='readwrite')

  ! Write the rows in reverse order: any record can be written at any time.
  do j = ny, 1, -1
    write (u, rec=j) field(:, j)
  end do

  inquire (unit=u, nextrec=next, recl=reclen, size=sz)
  print '(a, i0, a, i0, a, i0)', 'nextrec=', next, ' recl=', reclen, ' size=', sz

  ! Read row 3 first, then row 1: no sequential order is implied.
  read (u, rec=3) row
  print '(a, 5f5.1)', 'row 3: ', row
  read (u, rec=1) row
  print '(a, 5f5.1)', 'row 1: ', row

  ! Rewrite one record; a record cannot be deleted but can be replaced.
  write (u, rec=2) -row
  read (u, rec=2) row
  print '(a, 5f5.1)', 'row 2 after rewrite: ', row

  ! Record 9 was never written.
  read (u, rec=9, iostat=ios, iomsg=msg) row
  print '(a, i0, a)', 'read of record 9: iostat=', ios, ' iomsg=' // trim(msg)
  close (u, status='delete')
end program direct_access

GNU Fortran 16.2 and LLVM Flang 23 agree on every line except the last:

iolength of one row: 40 file storage units (320 bits)
nextrec=2 recl=40 size=160
row 3:  11.0 12.0 13.0 14.0 15.0
row 1:   1.0  2.0  3.0  4.0  5.0
row 2 after rewrite:  -1.0 -2.0 -3.0 -4.0 -5.0
read of record 9: iostat=5002 iomsg=Non-existing record number

LLVM Flang 23 prints the same lines and then read of record 9: iostat=-1 iomsg=End of file during input. A record that has never been written is simply not there; reading it is an error condition in gfortran (a positive iostat) and an end-of-file condition in Flang. Handle both by testing ios /= 0. Other points:

  • The record length is in file storage units for unformatted files and in characters for formatted ones. The iolength is 40 for five real64 values on both compilers.

  • nextrec is n + 1 where n is the number of the last record read or written, and 1 for a connection that has not been used, so after writing records 4, 3, 2, 1 it reports 2 (12.10.2.18). For connections that are not direct access it is undefined, so ask only for direct files.

  • Writing a shorter list than the record leaves the remainder undefined for an unformatted file and blank-fills a formatted one (12.6.4.5.2 and 12.6.4.5.3); writing a longer list is an error.

  • The file size is recl times the highest record written (here 160) on both compilers, but the standard leaves the relationship processor dependent.

Stream access

Stream access (12.3.3.4, Fortran 2003) is the model of C’s fopen and Python’s binary files. The file is a sequence of file storage units numbered from 1; the position of the next transfer is given by pos= in a read or write statement, and the current position is available as inquire (unit=u, pos=p). There are no record markers and no recl=. An unformatted stream file contains exactly the bytes of the items written, in order, which makes it the right format for data that another program (or language) will read, for in-place updates and for memory-mapped designs. The file size from inquire (size=) is exact. A formatted stream behaves like a text file in which pos= may be used, but only with 1 or with a value that inquire (pos=) returned before, because line ends may take more than one position on some systems.

The program below writes the heatflow checkpoint header and field, reads the header back, jumps to one element, updates it in place, and then shows a formatted stream.

program stream_access
  use, intrinsic :: iso_fortran_env, only: real64, int32, iostat_end
  implicit none
  integer, parameter :: dp = real64, ip = int32
  integer(ip) :: nx, ny, nx_back, ny_back
  real(dp) :: dx, dy, dx_back, dy_back
  real(dp), allocatable :: u_field(:, :), back(:, :)
  integer :: u, ios, p, sz, i
  character(len=64) :: line

  nx = 4_ip; ny = 3_ip; dx = 0.25_dp; dy = 0.5_dp
  allocate (u_field(nx, ny))
  u_field = reshape([(real(i, dp), i=1, 12)], [4, 3])

  ! Unformatted stream: no record markers, exactly the bytes of the items, in order.
  open (newunit=u, file='field.bin', access='stream', form='unformatted', status='replace', &
        action='readwrite')
  write (u) nx, ny, dx, dy        ! 2 x 4 + 2 x 8 = 24 file storage units
  write (u) u_field               ! 12 x 8 = 96 file storage units
  inquire (unit=u, pos=p, size=sz)
  print '(a, i0, a, i0)', 'next position=', p, ' file size=', sz

  ! Read the header back, then jump straight to a single element with POS=.
  read (u, pos=1) nx_back, ny_back, dx_back, dy_back
  print '(a, 2(i0, 1x), 2f5.2)', 'header: ', nx_back, ny_back, dx_back, dy_back
  allocate (back(nx_back, ny_back))
  read (u) back
  print '(a, l1)', 'whole field read back identical: ', all(abs(back - u_field) < 1.0e-12_dp)

  ! Element (3, 2) is the 7th value of the field: 24 header units + 6 values x 8 units, so POS = 73.
  read (u, pos=24 + 6 * 8 + 1) back(1, 1)
  print '(a, f5.1)', 'element (3,2) read by position: ', back(1, 1)

  ! Overwrite that element in place; the rest of the file is untouched.
  write (u, pos=24 + 6 * 8 + 1) -1.0_dp
  read (u, pos=1 + 24) back
  print '(a, f5.1, a, f5.1)', 'after the update element (3,2) is ', back(3, 2), &
    ' and element (2,2) is still ', back(2, 2)

  ! Reading past the last item is an end-of-file condition.
  read (u, iostat=ios) back(1, 1)
  print '(a, i0, a, l1)', 'read past the end: iostat=', ios, ' is end-of-file: ', ios == iostat_end
  close (u, status='delete')

  ! Formatted stream: text with record markers (here, a line feed), but byte-addressable.
  open (newunit=u, file='note.txt', access='stream', form='formatted', status='replace', &
        action='readwrite')
  write (u, '(a)') 'first line'
  write (u, '(a)') 'second line'
  inquire (unit=u, pos=p, size=sz)
  print '(a, i0, a, i0)', 'formatted stream: next position=', p, ' size=', sz
  read (u, '(a)', pos=1) line
  print '(a)', 'read from position 1: ' // trim(line)
  read (u, '(a)') line
  print '(a)', 'next record: ' // trim(line)
  close (u, status='delete')
end program stream_access

Both compilers print exactly the same lines:

next position=121 file size=120
header: 4 3  0.25 0.50
whole field read back identical: T
element (3,2) read by position:   7.0
after the update element (3,2) is  -1.0 and element (2,2) is still   6.0
read past the end: iostat=-1 is end-of-file: T
formatted stream: next position=24 size=23
read from position 1: first line
next record: second line

The arithmetic is simple and worth checking by hand: the header is two default integers (8 units) and two real64 values (16 units), 24 units in all; the field is 12 values of 8 units, 96 in all; the next position is 24 + 96 + 1 = 121 and the file size is 120. Element (3,2) is the seventh value in column-major order (see Arrays Fundamentals), so it begins at 24 + 6 * 8 + 1 = 73. A read beyond the last unit is an end-of-file condition, with iostat_end. The formatted stream example shows that the size of 23 characters is the two lines plus two line ends on this platform (a line end is one character on Linux and may be two on Windows). Source: J3/23-007r1: 12.3.3.4 Stream access, GCC wiki: Fortran 2003 status

The restrictions of stream files (12.8.1): backspace is not allowed on an unformatted stream; endfile makes the current position the new end of file; a read past the end is an end-of-file condition, not an end-of-record condition, and a formatted stream can be read non-advancing. Stream I/O is also the right tool to read a file you know nothing about, byte by byte: open it with access='stream' and read integer(int8) or character values.

Unformatted sequential files: record markers and portability

An unformatted sequential file stores each write as one record. The standard does not say how the record boundaries are stored; the usual implementation puts a length marker before and after the data, so that backspace and reading work. GNU Fortran documents its layout: each record has a leading and a trailing record marker, four bytes by default (-frecord-marker=8 for older files; -fmax-subrecord-length is for testing), and a record longer than about 2 GiB is split into subrecords. The next program looks at the real bytes through stream access.

program unformatted_sequential
  use, intrinsic :: iso_fortran_env, only: int32
  implicit none
  integer(int32) :: data(3), raw(5), n
  integer :: u, sz, ios
  character(len=100) :: msg
  integer(int32) :: a, b

  data = [10_int32, 20_int32, 30_int32]

  ! One WRITE statement is one record, whatever the number of items.
  open (newunit=u, file='seq.dat', form='unformatted', access='sequential', status='replace')
  write (u) data
  write (u) 99_int32
  close (u)

  ! Look at the same file as raw bytes through stream access.
  open (newunit=u, file='seq.dat', form='unformatted', access='stream', status='old', action='read')
  inquire (unit=u, size=sz)
  print '(a, i0)', 'file size in bytes: ', sz
  read (u) raw
  print '(a, 5(i0, 1x))', 'first record as raw int32 words: ', raw
  close (u)

  ! Reading with a different list than the one written: fewer items is allowed ...
  open (newunit=u, file='seq.dat', form='unformatted', status='old', action='read')
  read (u) a, b
  print '(a, 2(i0, 1x))', 'two items from a three-item record: ', a, b
  ! ... and the rest of the record is skipped by the next READ. Asking for more is an error.
  read (u, iostat=ios, iomsg=msg) data, n
  print '(a, i0, a)', 'next record is shorter than the list: iostat=', ios, ' iomsg=' // trim(msg)
  close (u, status='delete')
end program unformatted_sequential

GNU Fortran 16.2:

file size in bytes: 32
first record as raw int32 words: 12 10 20 30 12
two items from a three-item record: 10 20
next record is shorter than the list: iostat=5016 iomsg=I/O past end of record on unformatted file

LLVM Flang 23 prints the same first three lines and reports iostat=1003 for the last read. The first record holds 3 words of data, so the file size is 12 + 8 for the markers, plus 4 + 8 for the second record = 32 bytes, and the raw words are the marker 12, the data 10, 20, 30 and the marker 12. Both compilers use the same four-byte marker, but other processors may use another layout, and the standard promises nothing. This is the whole portability story of unformatted sequential files:

  • Between compilers. The marker size, the byte order and the representation of types such as logical and real(16) can differ. A file written by one compiler is not guaranteed to be readable by another, not even with the same marker width.

  • Between machines. Byte order (endianness) and the size of the default integer may differ.

  • Between languages. C, Python and MATLAB read a record file only if they know about the markers; they read a stream file directly.

  • Between versions. A compiler may change its marker width, as GNU Fortran did when its default marker went from 8 to 4 bytes; -frecord-marker=8 reads the old files.

The rules for reading are also stricter than for text: reading fewer items than the record contains is allowed and the rest of the record is skipped, but asking for more items than were written is an error (12.6.4.5.2). A mismatch of types is processor dependent, so the reader must know the writer’s list.

GNU Fortran and Flang offer a non-standard way to cope with byte order: the convert= specifier of open, the -fconvert= compiler option and (GNU Fortran) the GFORTRAN_CONVERT_UNIT environment variable, with the values native, swap, big_endian and little_endian. The specifier is a GNU extension and is rejected under -std=f2023.

! Non-standard but widely supported: CONVERT= selects the byte order of an unformatted file.
program convert_extension
  use, intrinsic :: iso_fortran_env, only: int32, int8
  implicit none
  integer(int8) :: b(4)
  integer :: u
  open (newunit=u, file='be.bin', form='unformatted', access='stream', convert='big_endian', &
        status='replace')
  write (u) 1_int32
  close (u)
  open (newunit=u, file='be.bin', form='unformatted', access='stream', status='old', &
        convert='native')
  read (u) b
  print '(a, 4(i0, 1x))', 'bytes of the integer 1 written with convert=big_endian: ', b
  close (u, status='delete')
end program convert_extension

Compiled without -std=f2023 both compilers produce the same output:

bytes of the integer 1 written with convert=big_endian: 0 0 0 1

The recommendation follows. A file that must outlive the program, cross a machine or a language, or be read by somebody else’s code is an unformatted stream file with a documented layout (a header with a magic number, a version and the sizes, followed by the arrays), or a self-describing library format such as NetCDF or HDF5. A file that two builds of the same program exchange can be sequential unformatted. A text file whose precision matters should use enough digits.

Formatted precision: how many digits survive

The CSV of heatflow uses es12.5: five significant digits, enough to plot but not to restart a simulation. The next program measures the round trip.

program roundtrip
  use, intrinsic :: iso_fortran_env, only: real64, int64
  implicit none
  integer, parameter :: dp = real64
  real(dp) :: x, y
  integer :: u
  character(len=40) :: text

  x = 1.0_dp / 3.0_dp
  open (newunit=u, status='scratch', form='formatted')

  ! 5 significant digits (the heatflow CSV format) is not enough to restore the value.
  write (u, '(es12.5)') x
  rewind (u); read (u, *) y
  print '(a, es10.2)', 'es12.5 round trip, |error| = ', abs(y - x)

  ! 17 significant digits restore every real(dp) exactly.
  rewind (u)
  write (u, '(es24.16e3)') x
  rewind (u); read (u, *) y
  print '(a, l1)', 'es24.16e3 round trip bit-identical: ', same_bits(x, y)

  ! g0 is compiler dependent in its choice of digits and in the leading zero.
  write (text, '(g0)') x
  print '(a)', 'g0 gives: ' // trim(text)
  close (u)

  ! An unformatted stream copies the bits.
  open (newunit=u, status='scratch', form='unformatted', access='stream')
  write (u) x
  read (u, pos=1) y
  print '(a, l1)', 'stream round trip bit-identical: ', same_bits(x, y)
  close (u)

contains

  logical function same_bits(a, b)
    real(dp), intent(in) :: a, b
    same_bits = transfer(a, 0_int64) == transfer(b, 0_int64)
  end function same_bits
end program roundtrip

GNU Fortran 16.2:

es12.5 round trip, |error| =   3.33E-07
es24.16e3 round trip bit-identical: T
g0 gives: 0.33333333333333331
stream round trip bit-identical: T

LLVM Flang 23 gives the same values except the last digits of g0: it prints .3333333333333333, with no leading zero and fewer digits. Seventeen significant digits (es24.16e3 here) restore every real64 exactly; fewer digits are lossy, and g0 is not a round-trip format on every compiler. The stream file is bit-identical by construction, which is why the heatflow checkpoint is a stream file and its CSV is only for plotting. The real kind’s digits and precision (see Numeric Model, Bit Manipulation and Conversion) tell how many digits a kind needs.

The heatflow I/O module

The module heatflow_io of the fpm project collects the three output routines. The listing is the real source of the project; it was compiled and run with fpm run, which executes the application on the configuration heatflow.nml.

module heatflow_io
  use heatflow_kinds, only: dp, ip
  use heatflow_grid, only: grid_t, make_grid
  implicit none
  private
  public :: write_csv, write_stream, read_stream

contains

  !> Write the temperature field as CSV (one row per j).
  subroutine write_csv(grid, file)
    type(grid_t), intent(in) :: grid
    character(len=*), intent(in) :: file
    integer :: unit, j
    open (newunit=unit, file=file, status='replace', action='write')
    do j = 1, grid%ny
      write (unit, '(*(es12.5,:,","))') grid%u(:, j)
    end do
    close (unit)
  end subroutine write_csv

  !> Unformatted stream: nx, ny, dx, dy, then the field.
  subroutine write_stream(grid, file)
    type(grid_t), intent(in) :: grid
    character(len=*), intent(in) :: file
    integer :: unit
    open (newunit=unit, file=file, access='stream', form='unformatted', &
          status='replace', action='write')
    write (unit) grid%nx, grid%ny, grid%dx, grid%dy
    write (unit) grid%u
    close (unit)
  end subroutine write_stream

  function read_stream(file) result(grid)
    character(len=*), intent(in) :: file
    type(grid_t) :: grid
    integer :: unit
    open (newunit=unit, file=file, access='stream', form='unformatted', &
          status='old', action='read')
    read (unit) grid%nx, grid%ny, grid%dx, grid%dy
    allocate (grid%u(grid%nx, grid%ny))
    read (unit) grid%u
    close (unit)
  end function read_stream
end module heatflow_io

Output of the application and a look at the two files it wrote (wc counts bytes and lines; od shows the first values of the binary file):

steps=2000 residual= 1.38E-04
max temperature=   0.139
3552 heatflow.bin
5733 heatflow.csv
21
0000000          21          21
0000008                     0.05                     0.05
 0.00000E+00, 0.00000E+00, 0.00000E+00,

The checkpoint size is 24 + 21 * 21 * 8 = 3552 bytes: a header of two default integers and two real64 values, followed by the 441 temperatures. The CSV has 21 lines of 272 characters each (21 fields of 12 characters and 20 commas) plus the line end, 5733 bytes in all. write_stream is the format to use for a restart; read_stream shows the symmetric read, with the shape taken from the header so the caller does not need to know it in advance. Source: J3/23-007r1: 12.6.4.5 Data transfer and 12.3.5 File storage units

Positioning: rewind, backspace, endfile and flush

The positioning statements (12.8) apply to units connected for sequential access (stream files allow rewind and endfile too, but not backspace when unformatted; direct access units allow none of them). All three take a unit (rewind u, rewind (u, iostat=ios)) and a position specifier list with iostat=, iomsg= and err=.

Statement Effect (clause)

rewind

positions the file at its initial point; no effect if already there or if the file does not exist (12.8.4)

backspace

positions before the current record, or before the preceding record if there is no current record; at the initial point it has no effect; it is prohibited after list-directed or namelist writes (12.8.2)

endfile

writes an endfile record, makes it the last record and positions after it; rewind or backspace is needed before the next transfer; on a stream file it sets the end of file to the current position (12.8.3)

flush

makes written data available to other processes, or externally written data available to the program; processor dependent (12.9)

An output statement that is followed by rewind, backspace, close or normal termination writes an endfile record implicitly (12.2.4); that is why a file you write and then rewind can be read back to its end. The program below shows backspace replacing the last record, endfile, a read to the end with iostat, position='append', flush and rewind.

program positioning
  implicit none
  integer :: u, ios, n
  character(len=20) :: line

  open (newunit=u, file='steps.log', status='replace', action='readwrite')
  write (u, '(a)') 'step 1'
  write (u, '(a)') 'step 2'
  write (u, '(a)') 'step 3'

  ! BACKSPACE positions the file before the record just written, so the next WRITE replaces it.
  backspace (u)
  write (u, '(a)') 'step 3 (rewritten)'

  ! ENDFILE writes an endfile record; the file must be repositioned before the next transfer.
  endfile (u)
  rewind (u)
  n = 0
  do
    read (u, '(a)', iostat=ios) line
    if (ios /= 0) exit
    n = n + 1
    print '(a, i0, a)', 'record ', n, ': ' // trim(line)
  end do
  print '(a, i0)', 'iostat at the end: ', ios
  close (u)

  ! POSITION='append' on an existing file continues after its last record.
  open (newunit=u, file='steps.log', status='old', position='append', action='write')
  write (u, '(a)') 'step 4'
  flush (u)                       ! make the data visible to other processes now
  close (u)

  open (newunit=u, file='steps.log', status='old', action='read')
  n = 0
  do
    read (u, '(a)', iostat=ios) line
    if (ios /= 0) exit
    n = n + 1
  end do
  print '(a, i0)', 'records after appending: ', n

  ! REWIND repeats the read from the initial point.
  rewind (u)
  read (u, '(a)') line
  print '(a)', 'first record again: ' // trim(line)
  close (u, status='delete')
end program positioning

Both compilers print the same lines:

record 1: step 1
record 2: step 2
record 3: step 3 (rewritten)
iostat at the end: -1
records after appending: 4
first record again: step 1

backspace followed by a write replaced the third record, because a sequential write makes the written record the last in the file (12.3.4.3). position='append' is the standard way to add to a log; status='old' makes sure the log exists and status='unknown' creates it otherwise. backspace on a unit opened with action='write' may fail on some processors that need to read the file to find the previous record (C.8.3), another reason to open log files with readwrite or to avoid backspace on them. Source: J3/23-007r1: 12.8 File positioning statements, 12.9 FLUSH statement and 12.3.4.3 File position prior to data transfer

flush and the buffering of unit output

GNU Fortran buffers regular files and flushes the buffer when it is full, at close and at flush. flush (u) asks the processor to write the buffered data so that another process (a tail -f on a log, a parent shell) can see it. It does not guarantee that the data reaches the disk. GNU Fortran states that it makes no effort to commit data to stable storage and shows the route to do so: obtain the file descriptor with the extension fnum, call fsync through iso_c_binding, after a flush. Its buffering can be turned off with the environment variables GFORTRAN_UNBUFFERED_ALL and GFORTRAN_UNBUFFERED_PRECONNECTED, which slows small reads and writes; terminals and pipes are always unbuffered. Use flush (output_unit) before a long computation when a progress message must appear, and before calling a C routine or execute_command_line that writes to the same stream. The termination statements are in Error Handling and Program Termination. Source: GNU Fortran manual: Data consistency and durability, GNU Fortran manual: GFORTRAN_UNBUFFERED_ALL

Error handling: iostat, iomsg, err, end and eor

Every I/O statement can fail, and an input/output statement can raise three kinds of condition (12.11.1): an error, an end-of-file (reading past the endfile record, past the end of an internal file or past the end of a stream) and an end-of-record (a non-advancing read that needs more characters than the record has). What happens depends on which specifiers the statement carries:

Condition Without iostat= and the matching label With them

error

error termination of the program (12.11.2)

processing of the list stops; iostat becomes a positive, processor-dependent value; iomsg gets a message; err= branches; the file position is indeterminate and the items of a read are undefined

end of file

error termination (12.11.3)

iostat becomes iostat_end (negative); iomsg gets a message; end= branches; an external record file is positioned after its endfile record

end of record

error termination (12.11.4)

iostat becomes iostat_eor (negative); with pad='yes' the item is padded with blanks; eor= branches; the file is positioned after the record

iostat= is zero for success, positive for an error, negative for an end-of-file (iostat_end) or end-of-record (iostat_eor) condition (12.11.5); for flush a negative value other than these two means the processor does not support flushing the unit. For inquire with an internal unit, the value is iostat_inquire_internal_unit. The constants come from iso_fortran_env (16.10.2.16 to 16.10.2.18). The iomsg= variable receives an explanatory message if a condition occurs, and is unchanged otherwise (12.11.6), so initialise it if you read it unconditionally. The labels err=, end= and eor= are not obsolescent: they are fully standard, and the books that call them obsolescent are wrong on that point, but iostat= is the usual choice because it does not break the structured flow of the procedure. A statement that has both iostat= and end= gets both effects on an end-of-file condition: the variable is set and the branch is taken (12.11.3). err=, end= and eor= labels must name a branch target in the same scope (C1206, C1215). Only the statement that carried the specifier has the protected behaviour, not the statements that follow, which is why every statement in an I/O sequence needs its own iostat=.

program error_handling
  use, intrinsic :: iso_fortran_env, only: iostat_end, iostat_eor
  implicit none
  integer :: u, ios, n
  real :: x
  character(len=200) :: msg
  character(len=20) :: text

  ! 1. Opening a file that does not exist with status='old'.
  open (newunit=u, file='does-not-exist.dat', status='old', action='read', iostat=ios, iomsg=msg)
  print '(a, i0, a)', '1. open: iostat=', ios, ' iomsg=' // trim(msg)

  ! 2. Reading a value that is not a number from an internal file.
  text = 'abc'
  read (text, *, iostat=ios, iomsg=msg) n
  print '(a, i0, a)', '2. read: iostat=', ios, ' iomsg=' // trim(msg)

  ! 3. Writing to a unit that was opened for reading only.
  open (newunit=u, file='ro.txt', status='replace', action='write')
  write (u, '(a)') '1.5'
  close (u)
  open (newunit=u, file='ro.txt', status='old', action='read')
  write (u, '(a)', iostat=ios, iomsg=msg) 'not allowed'
  print '(a, i0, a)', '3. write: iostat=', ios, ' iomsg=' // trim(msg)

  ! 4. End of file: a negative iostat, no error condition. (The position after the failed
  !    write is indeterminate, so reposition first.)
  rewind (u)
  read (u, *, iostat=ios, iomsg=msg) x
  print '(a, i0, a, f4.1)', '4a. read: iostat=', ios, ' x=', x
  read (u, *, iostat=ios, iomsg=msg) x
  print '(a, i0, a, l1, a)', '4b. read: iostat=', ios, ' end of file=', ios == iostat_end, &
    ' iomsg=' // trim(msg)

  ! 5. The same cases with ERR= and END= labels instead of IOSTAT=.
  rewind (u)
  read (u, *, err=90, end=91) x
  read (u, *, err=90, end=91) x
  print '(a)', '5. not reached'
90 print '(a)', '5. err= branch'
  stop
91 print '(a)', '5. end= branch taken at end of file'
  close (u, status='delete')
end program error_handling

GNU Fortran 16.2 (the iostat values are 2 for a missing file, 5010 for a bad integer and 5007 for a write on a read-only unit):

1. open: iostat=2 iomsg=Cannot open file 'does-not-exist.dat': No such file or directory
2. read: iostat=5010 iomsg=Bad integer for item 1 in list input
3. write: iostat=5007 iomsg=Cannot write to file opened for READ
4a. read: iostat=0 x= 1.5
4b. read: iostat=-1 end of file=T iomsg=End of file
5. end= branch taken at end of file

LLVM Flang 23:

1. open: iostat=2 iomsg=No such file or directory
2. read: iostat=1001 iomsg=Bad character 'a' in INTEGER input field
3. write: iostat=1012 iomsg=Attempted output to read-only file
4a. read: iostat=0 x= 1.5
4b. read: iostat=-1 end of file=T iomsg=End of file during input
5. end= branch taken at end of file

The only portable facts are the signs of iostat and the constants iostat_end and iostat_eor. The positive codes (2, 5010, 5007 in gfortran; 2, 1001, 1012 in Flang) and the texts of iomsg are the processor’s own. Never match the text of iomsg or compare iostat with a number, except zero: a library that needs to react to a missing file does an inquire (file=..., exist=...) or looks at iostat /= 0 and reports iomsg. iomsg must be a variable long enough for the message; a short variable truncates. Fortran 2023 relaxes that, in principle: an iomsg variable may be a deferred-length allocatable character that the processor allocates (see the table of standard versions below), but neither compiler supports it yet, as the next, deliberately problematic, example shows.

! Fortran 2023: a deferred-length allocatable character variable can receive IOMSG= and be an internal file.
program allocatable_iomsg
  implicit none
  integer :: u, ios
  character(len=:), allocatable :: msg, text
  open (newunit=u, file='missing-file.dat', status='old', iostat=ios, iomsg=msg)
  print '(a, i0, a, i0)', 'iostat=', ios, ' message length=', len(msg)
  print '(a)', msg
  write (text, '(a, i0, a, f6.2)') 'steps=', 2000, ' residual=', 0.01
  print '(a, i0, a)', 'internal write allocated ', len(text), ' characters: ' // text
end program allocatable_iomsg

GNU Fortran 16.2 warns at compile time that msg may be used uninitialised and the program fails at run time:

Warning: '.msg' may be used uninitialized [-Wmaybe-uninitialized]
Warning: '.text' may be used uninitialized [-Wmaybe-uninitialized]

Program received signal SIGSEGV: Segmentation fault - invalid memory reference.

Backtrace for this error:

LLVM Flang 23 compiles it, and leaves msg unallocated, so the first print shows a length of 0 and the second fails:

iostat=2 message length=0

fatal Fortran runtime error(/work/14_allocatable_iomsg.f90:8): Null address for character output item
Aborted

Use a fixed-length character(len=256) variable for iomsg today. Flang’s standards-support table lists "Automatic allocation of lengths of character variables" as not implemented. Source: J3/23-007r1: 12.11 Error, end-of-record, and end-of-file conditions, Flang documentation: Fortran Standards Support

What happens without iostat

An unhandled condition is error termination: the run stops with a message, with a non-zero exit status and, with a compiler that supports it, a backtrace. The three programs below have no iostat= and fail on purpose. GNU Fortran 16.2 prints a header with the source location, the message and then a backtrace, which is cut here; the exit status is 2. LLVM Flang 23 prints one line and aborts with exit status 134.

An open of a missing file with status='old':

! INTENTIONAL RUNTIME ERROR: no IOSTAT=, ERR= or IOMSG= on the OPEN.
program fatal_open
  implicit none
  integer :: u
  open (newunit=u, file='does-not-exist.dat', status='old', action='read')
  print '(a)', 'not reached'
end program fatal_open

GNU Fortran 16.2:

At line 5 of file 05a_fatal_open.f90
Fortran runtime error: Cannot open file 'does-not-exist.dat': No such file or directory

LLVM Flang 23:

fatal Fortran runtime error(/work/05a_fatal_open.f90:5): I/O error (errno=2): No such file or directory
Aborted

A read past the end of a file:

! INTENTIONAL RUNTIME ERROR: reading past the end of a file without END= or IOSTAT=.
program fatal_read
  implicit none
  integer :: u, n
  open (newunit=u, status='scratch')
  write (u, '(i0)') 42
  rewind (u)
  read (u, *) n
  read (u, *) n
  print '(a)', 'not reached'
end program fatal_read

GNU Fortran 16.2:

At line 9 of file 05b_fatal_read.f90
Fortran runtime error: End of file

LLVM Flang 23:

fatal Fortran runtime error(/work/05b_fatal_read.f90:9): End of file during input
Aborted

A conversion error in a formatted read:

! INTENTIONAL RUNTIME ERROR: a text field that is not an integer, no IOSTAT=.
program fatal_conversion
  implicit none
  integer :: n
  character(len=8) :: text
  text = '12x'
  read (text, '(i8)') n
  print '(a)', 'not reached'
end program fatal_conversion

GNU Fortran 16.2:

At line 7 of file 05c_fatal_conversion.f90
Fortran runtime error: Bad value during integer read

LLVM Flang 23:

fatal Fortran runtime error(/work/05c_fatal_conversion.f90:7): Bad character 'x' in INTEGER input field
Aborted

The messages are not the same, which is the reason for programs to supply iostat and iomsg and to compose their own error text with the file name and operation. Do not use stop with the iomsg text for a library; return the status. The termination statements and exit codes are covered in Error Handling and Program Termination.

Connection modes, encoding and the Fortran 2023 leading zero

A connection for formatted input and output has a set of changeable modes (12.5.2): blank interpretation, delimiter, sign, leading zero, decimal edit, rounding and pad, and the scale factor (set only by the p edit descriptor). A connection for unformatted I/O has none. They have three layers, from the longest-lived to the shortest: the value given by open (or the default), a later open on the unit with no file= that changes it, a keyword in one data transfer statement, and an edit descriptor inside a format. When a data transfer statement ends, the modes revert to the connection’s values (12.5.2). The next program opens with decimal='comma', sign='plus' and delim='quote', overrides modes for one statement, changes the connection’s modes with a second open, and then connects a UTF-8 file.

program connection_modes
  implicit none
  integer :: u
  character(len=40) :: line
  character(len=20) :: dec, sgn, delim, pad
  integer, parameter :: ucs4 = selected_char_kind('ISO_10646')
  character(kind=ucs4, len=12) :: wide

  ! Modes are part of the connection: set them in OPEN ...
  open (newunit=u, file='modes.txt', status='replace', action='readwrite', &
        decimal='comma', sign='plus', delim='quote')
  write (u, '(f8.3)') 3.14159
  write (u, *) 'text', 2.5
  ! ... override them for one statement ...
  write (u, '(f8.3)', decimal='point', sign='suppress') 3.14159

  ! ... or change the changeable ones with a second OPEN on the same unit and no FILE=.
  open (unit=u, decimal='point', sign='processor_defined', status='old', delim='none')
  write (u, '(f8.3)') 2.71828
  inquire (unit=u, decimal=dec, sign=sgn, delim=delim, pad=pad)
  print '(a)', 'decimal=' // trim(dec) // ' sign=' // trim(sgn) // ' delim=' // trim(delim) &
    // ' pad=' // trim(pad)

  rewind (u)
  do
    read (u, '(a)', end=10) line
    print '(a)', '|' // trim(line) // '|'
  end do
10 continue
  close (u, status='delete')

  ! A UTF-8 file: ENCODING= together with the ISO 10646 character kind.
  if (ucs4 > 0) then
    open (newunit=u, file='utf8.txt', status='replace', encoding='UTF-8', action='readwrite')
    wide = ucs4_'Temperatur ' // char(int(z'00B0'), ucs4)
    write (u, '(a)') trim(wide)
    rewind (u)
    wide = ucs4_''
    read (u, '(a)') wide
    print '(a, i0, a, i0)', 'UTF-8 record read back, characters=', len_trim(wide), &
      ' code of the last: ', iachar(wide(12:12))
    close (u, status='delete')
  else
    print '(a)', 'ISO 10646 characters are not supported'
  end if
end program connection_modes

GNU Fortran 16.2:

decimal=POINT sign=PROCESSOR_DEFINED delim=NONE pad=YES
|  +3,142|
| "text"  +2,50000000|
|   3.142|
|   2.718|
UTF-8 record read back, characters=12 code of the last: 176

LLVM Flang 23:

decimal=POINT sign=SUPPRESS delim=NONE pad=YES
|  +3,142|
| "text" +2,5|
|   3.142|
|   2.718|
UTF-8 record read back, characters=12 code of the last: 176

The two compilers agree on every explicit mode, but they differ in two places. Inquiring sign= after sign='processor_defined' gives PROCESSOR_DEFINED with gfortran and SUPPRESS with Flang, and a list-directed 2.5 is written as +2,50000000 by gfortran and +2,5 by Flang, because list-directed output (*) is not specified digit by digit: the processor chooses the width and the number of digits (13.10). Use an explicit format for output that other programs parse. The delim='quote' mode makes list-directed character output come out in double quotes, and decimal='comma' is the locale-style choice used in many European CSV files (see Format Specifications and Edit Descriptors, which also covers the dc and dp control edit descriptors).

The encoding='utf-8' specifier connects a formatted file whose characters are of the ISO 10646 kind; the character kind comes from selected_char_kind ('ISO_10646') and the file is converted on input and output. Both compilers read the UTF-8 file back as 12 characters, the last being the degree sign (code 176), although that character takes two bytes in the UTF-8 file. Unicode and the character kinds are described in Characters and Strings.

leading_zero: the Fortran 2023 specifier

Fortran 2023 adds the leading_zero= specifier (12.5.6.12, 12.6.2.10 and 12.10.2.15) and the lz, lzp and lzs control edit descriptors (13.8.5) to control whether the optional zero before the decimal point is written in output such as f6.3 of 0.5. The values are print, suppress and processor_defined. The feature is listed in the Introduction of the standard as new in Fortran 2023. LLVM Flang 23 supports it and GNU Fortran 16.2 does not.

! Fortran 2023 LEADING_ZERO=: accepted by LLVM Flang 23, rejected by GNU Fortran 16.2.
program leading_zero_f2023
  implicit none
  integer :: u
  character(len=20) :: mode
  open (newunit=u, file='lz.txt', status='replace', action='readwrite', leading_zero='suppress')
  inquire (unit=u, leading_zero=mode)
  print '(a)', 'leading_zero mode of the connection: ' // trim(mode)
  write (u, '(f6.3)') 0.5
  write (u, '(f6.3)', leading_zero='print') 0.5       ! one statement overrides the mode
  write (u, '(lzp, f6.3)') 0.5                          ! so does a control edit descriptor
  rewind (u)
  read (u, '(a)') mode
  print '(a)', '|' // trim(mode) // '|'
  read (u, '(a)') mode
  print '(a)', '|' // trim(mode) // '|'
  read (u, '(a)') mode
  print '(a)', '|' // trim(mode) // '|'
  close (u, status='delete')
end program leading_zero_f2023

LLVM Flang 23:

leading_zero mode of the connection: SUPPRESS
|  .500|
| 0.500|
| 0.500|

GNU Fortran 16.2 rejects every occurrence with a syntax error (first diagnostic for each statement; the compilation stops at the errors):

Error: Syntax error in OPEN statement at (1)
11_leading_zero_f2023.f90:7:19:

    7 |   inquire (unit=u, leading_zero=mode)
--
Error: Syntax error in INQUIRE statement at (1)
11_leading_zero_f2023.f90:10:22:

   10 |   write (u, '(f6.3)', leading_zero='print') 0.5       ! one statement overrides the mode
--
Error: Syntax error in WRITE statement at (1)
11_leading_zero_f2023.f90:11:16:

Until the specifier is available in every compiler you use, do not depend on the optional zero in output that another program parses: the es and en edit descriptors always write a digit before the decimal point, and g0 showed above that the zero is a compiler choice. Source: J3/23-007r1: 12.5.6.12 LEADING_ZERO= specifier in the OPEN statement and 13.8.5 Leading zero mode, Flang documentation: Fortran Standards Support

Practical rules for files

  • Always say what you mean in open: status='old', action='read' for input, status='replace', action='write' for output, and iostat=/iomsg= on both. The default status='unknown' hides typing errors.

  • Prefer newunit= to numbers; put the unit in a derived type when it belongs to an object, and close it in a final procedure or an explicit close method (see Type-Bound Procedures, Abstract Types and Finalization).

  • Close what you open. The runtime closes all units at normal termination, but a library that opens and forgets leaks units for the rest of the run.

  • Give files meaningful names and let the caller choose them. File names are processor dependent in syntax, in the interpretation of case and in whether trailing blanks matter (12.5.6.10): pass names as character(len=*), trim them, and do not build paths with a hard-coded separator when the program should run on Windows and Linux.

  • Use a different file name on each image in coarray programs when the images write files (include this_image() in the name), because a name on two images is not necessarily the same file (12.3.1).

  • Scratch files go to the directory given by TMPDIR in GNU Fortran, then the system default; they disappear at close or at termination.

  • Use stream access for binary data and sequential formatted for text; reserve direct access for fixed-width databases.

  • Pass errors up. A routine that reads a file should return iostat and iomsg, with the file name added by the caller; only the main program decides to error stop.

  • Avoid inquire (exist=) as a guard. The check and the open race with other processes; open and test the iostat.

  • Do not rely on fort.N. A forgotten open is a bug, not a feature.

Where the books are outdated

The two Fortran books consulted for this section describe the I/O model completely and the statements still apply. Where they differ from current practice:

Area Outdated in the books, current approach

err= and end=

two footnotes call err= obsolescent and point to the appendix of deprecated features; in the standard err=, end= and eor= are not obsolescent (they are not in the obsolescent features of Annex B), and iostat= with iomsg= is the preferred style

Unit numbers

examples fix unit numbers by hand; newunit= (Fortran 2008) avoids clashes, and iso_fortran_env names the preconnected units

Binary files

the book treats sequential unformatted and stream files in the same breath; for portable binary data use stream files with a documented layout, and know the record marker, byte order and kind differences between compilers

Compiler context

no compiler support information; the I/O statements of this page work on GNU Fortran 16.2 and LLVM Flang 23, with the exceptions of leading_zero= (Flang only) and allocatable iomsg (neither)

Allocation of iomsg

Fortran 2023 allows a deferred-length allocatable iomsg variable and internal file; neither compiler implements it as of 2026-10-19

Standard version provenance and compiler support

Feature Introduced Compiler support checked on 2026-10-19

open, close, inquire, rewind, backspace, endfile, sequential and direct access, iostat=, err=, end=

long-standing core of the language; no edition is claimed here

all compilers

Stream access, asynchronous=, encoding=, decimal=, round=, sign=, pad=, blank=, delim=, the iso_fortran_env module with input_unit, output_unit, error_unit

Fortran 2003 (as listed on the GCC wiki page for Fortran 2003)

GNU Fortran 16.2 and LLVM Flang 23 accept the examples

is_iostat_end, is_iostat_eor

Fortran 2003 (GNU Fortran manual: "Fortran 2003 and later")

both

flush statement, iomsg=, iostat_end, iostat_eor

added after Fortran 95; the sources used for this page do not give the edition

both

newunit=

Fortran 2008

both

convert= on open, -fconvert, GFORTRAN_CONVERT_UNIT

extension of the compilers, not in the standard

both accept it; GNU Fortran rejects it under -std=f2023

leading_zero= in open, write and inquire, and the lz, lzp, lzs descriptors

Fortran 2023 (Introduction of J3/23-007r1)

Flang 23 accepts; GNU Fortran 16.2 rejects

Deferred-length allocatable iomsg= variable and internal file

Fortran 2023 (Introduction of J3/23-007r1)

neither GNU Fortran 16.2 (run-time segmentation fault) nor Flang 23 (variable left unallocated)

Not checked here, because no compiler for them is installed: Intel ifx, NAG, HPE Cray and NVIDIA HPC compilers. See History, Standards and Compilers for the per-compiler support table.

Pitfalls

  • status='unknown' creates a file when you misspell its name. Say old for input.

  • A write to a unit that was never opened creates fort.N with both compilers and no diagnostic.

  • Two statements, one iostat=. Each I/O statement needs its own iostat=; the unprotected one terminates the program.

  • Comparing iostat with a number other than zero is not portable; use iostat_end, is_iostat_end and iostat_eor.

  • Matching the text of iomsg breaks when the compiler changes.

  • iomsg shorter than the message truncates it, and a variable that was never assigned holds garbage: the standard leaves iomsg unchanged when no condition occurs, so read it only when iostat /= 0 or initialise it.

  • Reading until iostat /= 0 also stops at errors, not only at the end. Distinguish iostat_end from a positive value.

  • A read list longer than the record continues on the next record for list-directed input and fails for formatted input with pad='no'.

  • Reading again after an end of file. After the end-of-file condition on a file with an endfile record, the file is positioned after the endfile record; rewind or backspace is needed before the next read.

  • Unformatted sequential files are not portable binary files. Record markers, byte order and kinds differ; use stream files for data that must outlive the compiler.

  • A real written with few digits does not restart a simulation. Use 17 significant digits for real64, or a stream file.

  • Assuming the unit of recl= for unformatted files. The standard measures it in file storage units, which are bytes with both compilers here, but some compilers have counted it in other units by default (read the manual). Use inquire (iolength=) to compute it.

  • Writing the same file from two images or two units is a race the language does not protect you from.

  • position='append' with status='replace' replaces; append needs status='old' (or unknown) so that the file is kept.

  • Two units on one file is processor dependent: gfortran allows it, Flang does not.

  • nextrec= on a sequential file returns an undefined value; ask only for direct access.

  • flush is not fsync. It makes data visible to other processes, not durable.