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, |
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 |
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,inquireandwaitneeds a unit that is connected. Aclose,inquireorwaitmay 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 andrewind 6in 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_unitis preconnected only on image 1 of the initial team; each image has its ownoutput_unitanderror_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 |
|
preconnected for formatted input on image 1 only |
|
standard output |
|
preconnected for formatted output |
|
error output |
|
processor-dependent unit for error reports, which may be the same as the output unit |
none: write to |
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 |
|---|---|---|---|
|
non-negative integer |
none (or |
C1204 and C1205: exactly one of |
|
integer variable |
none |
gets a negative value, never -1 and never |
|
character expression |
processor-dependent for a new unit |
required with |
|
|
|
|
|
|
processor-dependent |
|
|
|
|
12.5.6.3 |
|
|
|
12.5.6.11 |
|
|
|
only for sequential and stream; |
|
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) |
|
integer variable, character variable, label |
none |
|
|
|
|
allows asynchronous transfers on the unit; see Derived-Type and Asynchronous I/O |
|
|
|
formatted connections only; |
|
|
|
how blanks in numeric input fields are read; changeable mode |
|
|
|
decimal symbol; changeable mode |
|
|
|
delimiter of character values in list-directed and namelist output; changeable mode |
|
|
|
whether short input records are padded with blanks; changeable mode |
|
|
processor-dependent |
rounding of formatted conversion; changeable mode |
|
|
|
sign of non-negative numbers on output; changeable mode |
|
|
|
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 areUNDEFINEDorUNKNOWN. -
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 checkiostaton theopenas well. -
Inquire by output list gives the length of unformatted records:
inquire (iolength=n) list. It is the portable way to computerecl=for direct access (12.10.3).
The specifiers (R1231, 12.10.2) fall into groups:
| Group | Specifiers | Meaning |
|---|---|---|
Existence and connection |
|
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 |
|
what |
Current modes |
|
the connection modes in effect (formatted connections only, else |
What the file allows |
|
|
Positions and sizes |
|
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 |
|
is a transfer still running (see Derived-Type and Asynchronous I/O) |
Error reporting |
|
as for the other statements; |
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.
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 |
|
the CSV output and the configuration ( |
sequential, unformatted |
one record per statement, with record markers around the data |
|
intermediate files exchanged between Fortran programs |
direct, unformatted |
fixed length, numbered from 1 ( |
|
a table of rows read by number |
direct, formatted |
fixed length text records |
|
rare; fixed-width databases |
stream, unformatted |
none; a sequence of file storage units |
|
the checkpoint ( |
stream, formatted |
text with record markers (line ends) but addressable |
|
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
writestatement 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 checkiostatand 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 constantiostat_end, the input list is undefined, and a file with an endfile record is positioned after it. Therewindthat follows is required before the file is read again. -
The messages in
iomsgare not standardised;iostatvalues 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
iolengthis 40 for fivereal64values on both compilers. -
nextrecisn + 1wherenis 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
recltimes 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
Source: J3/23-007r1: 12.2.3 Unformatted record and 12.6.4.5.2 Unformatted data transfer, GNU Fortran manual: File format of unformatted sequential files
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
logicalandreal(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
integermay 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=8reads 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
With gfortran -std=f2023 -pedantic the program is rejected: Error: GNU Extension: CONVERT tag at (1). Source: GNU Fortran manual: File format of unformatted sequential files, GNU Fortran manual: Influencing runtime behavior, GNU Fortran manual: GFORTRAN_CONVERT_UNIT, Flang documentation: Extensions
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) |
|---|---|
|
positions the file at its initial point; no effect if already there or if the file does not exist (12.8.4) |
|
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) |
|
writes an endfile record, makes it the last record and positions after it; |
|
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; |
end of file |
error termination (12.11.3) |
|
end of record |
error termination (12.11.4) |
|
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:
Source: J3/23-007r1: 12.5.6.12
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, andiostat=/iomsg=on both. The defaultstatus='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 afinalprocedure or an explicitclosemethod (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
TMPDIRin GNU Fortran, then the system default; they disappear atcloseor 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
iostatandiomsg, with the file name added by the caller; only the main program decides toerror stop. -
Avoid
inquire (exist=)as a guard. The check and theopenrace with other processes; open and test theiostat. -
Do not rely on
fort.N. A forgottenopenis 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 |
|---|---|
|
two footnotes call |
Unit numbers |
examples fix unit numbers by hand; |
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 |
Allocation of |
Fortran 2023 allows a deferred-length allocatable |
Standard version provenance and compiler support
| Feature | Introduced | Compiler support checked on 2026-10-19 |
|---|---|---|
|
long-standing core of the language; no edition is claimed here |
all compilers |
Stream access, |
Fortran 2003 (as listed on the GCC wiki page for Fortran 2003) |
GNU Fortran 16.2 and LLVM Flang 23 accept the examples |
|
Fortran 2003 (GNU Fortran manual: "Fortran 2003 and later") |
both |
|
added after Fortran 95; the sources used for this page do not give the edition |
both |
|
Fortran 2008 |
both |
|
extension of the compilers, not in the standard |
both accept it; GNU Fortran rejects it under |
|
Fortran 2023 (Introduction of J3/23-007r1) |
Flang 23 accepts; GNU Fortran 16.2 rejects |
Deferred-length allocatable |
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. Sayoldfor input. -
A
writeto a unit that was never opened createsfort.Nwith both compilers and no diagnostic. -
Two statements, one
iostat=. Each I/O statement needs its owniostat=; the unprotected one terminates the program. -
Comparing
iostatwith a number other than zero is not portable; useiostat_end,is_iostat_endandiostat_eor. -
Matching the text of
iomsgbreaks when the compiler changes. -
iomsgshorter than the message truncates it, and a variable that was never assigned holds garbage: the standard leavesiomsgunchanged when no condition occurs, so read it only wheniostat /= 0or initialise it. -
Reading until
iostat /= 0also stops at errors, not only at the end. Distinguishiostat_endfrom a positive value. -
A
readlist longer than the record continues on the next record for list-directed input and fails for formatted input withpad='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;
rewindorbackspaceis 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
realwritten with few digits does not restart a simulation. Use 17 significant digits forreal64, 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). Useinquire (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'withstatus='replace'replaces; append needsstatus='old'(orunknown) 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. -
flushis notfsync. It makes data visible to other processes, not durable.
See also Best Practices and Style and Common Gotchas and Pitfalls.
References
-
Fortran Programming Language: Best Practices, File Input/Output
-
GNU Fortran manual: Influencing runtime behavior and File format of unformatted sequential files
-
GNU Fortran manual: Data consistency and durability, Files opened without an explicit ACTION= specifier and Asynchronous I/O
-
GNU Fortran manual: TMPDIR, Directory for scratch files and the runtime environment variables GFORTRAN_CONVERT_UNIT and GFORTRAN_UNBUFFERED_ALL
-
Flang documentation: Extensions and Fortran Standards Support