Fortran stream and binary files
last modified November 14, 2026
The files article wrote text: digits, letters and line breaks that a person can read. A binary or unformatted file stores the values themselves, as the bytes the computer uses in memory, so an integer takes four bytes instead of a line of digits, and a real keeps its full precision. Such a file is smaller and faster, and reading it back gives exactly the values that were written. The price is that it is not portable in the same easy way: the byte order, called endianness, and the size of each type depend on the platform. This article shows the three ways Fortran organises binary data, the record markers that a sequential unformatted file carries, stream access for raw bytes, and direct access for records of a fixed length.
Unformatted sequential records
An open without form= produces a formatted file, so
binary files ask for form='unformatted' explicitly. A
write then transfers the values as they are stored, and each
statement creates one record. The program below writes four integers
in one record and three more in a second one, then reads both records back into
an array. Only the first three elements are read from the second record,
because the array has room for four values and the record holds three.
program b1
implicit none
integer :: u, i, v(4)
open (newunit=u, file='ints.bin', form='unformatted', status='replace')
write (u) 10, 20, 30, 40
write (u) (i, i = 1, 3)
close (u)
open (newunit=u, file='ints.bin', form='unformatted', status='old')
read (u) v
print *, 'first record:', v
read (u) v(1:3)
print *, 'second record:', v(1:3)
close (u)
end program b1
The values come back unchanged, and the second read fills only the part of the
array that the record can supply. A record read into a variable that is too
small is an error, so the size of each record must be known by both the writer
and the reader. That is the central rule of unformatted files: the program that
reads must use exactly the types and the order that the program that wrote
used. The file also holds more than the values: each record is surrounded by
two integers that give its length in bytes, the record markers that
let the runtime skip from one record to the next. The od command
prints the file one four-byte integer at a time and makes them visible.
The first 16 and the second 16 bracket the four values 10, 20, 30 and 40. Then
come 12 and 12, bracketing 1, 2 and 3, with nothing after the closing marker
because the file ends there. A text editor shows such a file as garbage,
because most of its bytes are not printable characters. The markers also
explain why an unformatted sequential file cannot be read by a program that
assumes a different marker size: they are integers of the platform, four bytes
on this one. The transcript below runs the program again and adds the
od dump, so the two views of the same file can be compared.
$ gfortran b1.f90 -o b1
$ ./b1; od -An -td4 ints.bin
first record: 10 20 30 40
second record: 1 2 3
16 10 20 30
40 16 12 1
2 3 12
Stream access
Stream access removes the markers and treats the file as one long sequence of
bytes, which is exactly what a file written by another language, or by a
library such as HDF5 or a plotting tool, usually contains. The specifier
access='stream' makes the position a byte offset, and a
write of an array stores its elements without any framing. The
program below writes four integers and reads them back in one statement.
program b2
implicit none
integer :: u
integer :: a(4), b(4)
a = [100, 200, 300, 400]
open (newunit=u, file='array.bin', access='stream', form='unformatted', &
status='replace')
write (u) a
close (u)
open (newunit=u, file='array.bin', access='stream', form='unformatted', &
status='old')
read (u) b
close (u)
print *, 'read back:', b
print *, 'sum: ', sum(b)
end program b2
The file holds sixteen bytes and the read fills all four elements, so the sum
is one thousand. Printing the bytes in hexadecimal shows the layout: each
integer is four bytes with the least significant byte first, which is the
little-endian order used by x86 and ARM processors. The value 100 is
64 00 00 00 and 200 is c8 00 00 00. On a
big-endian machine the same program would produce the bytes in the opposite
order, and a file copied between the two would need a byte swap.
Reading a stream file is the reverse of writing it: the bytes are taken in the
same order and interpreted with the same types, so the array comes back
unchanged. The inquire function can report how large the file is,
which is how a program discovers how many values a stream file holds before
allocating the array for them; the example with a file of unknown length later
in the article uses exactly that.
$ gfortran b2.f90 -o b2 $ ./b2; od -An -tx1 array.bin read back: 100 200 300 400 sum: 1000 64 00 00 00 c8 00 00 00 2c 01 00 00 90 01 00 00
Formatted stream files
Stream access can also be used with form='formatted'. The bytes
are then characters, and each write statement still ends its
record with a line break, but the position is counted in characters and can be
moved to any byte. This is the mode to use when a program must read a text file
from a particular offset, or write a character at a position without rewriting
the rest. The program below writes two short lines and asks how many bytes they
occupy.
program b3
implicit none
integer :: u, n
character(len=20) :: s
open (newunit=u, file='text.bin', access='stream', form='formatted', &
status='replace')
write (u, '(a)') 'Fortran'
write (u, '(a)') 'stream'
close (u)
inquire (file='text.bin', size=n)
print *, 'bytes on disk:', n
open (newunit=u, file='text.bin', access='stream', form='formatted', &
status='old')
read (u, '(a)') s
close (u)
print *, '[', trim(s), '] is', len_trim(s), 'characters'
end program b3
Seven letters, a line break, six letters and another line break make fifteen
bytes, which is what inquire(size=) reports. Reading with
'(a)' stops at the line break and gives the seven characters of
the first word. A stream file with a format still writes the record terminator,
so it is not identical to a raw byte stream; for a file without any line
breaks, write with a format that has no record ending, or use unformatted
stream access.
$ gfortran b3.f90 -o b3 $ ./b3 bytes on disk: 15 [Fortran] is 7 characters
Direct access to fixed-length records
Direct access gives every record the same length and a number, so a
program can read or write any record without touching the ones before it. The
length is declared with recl=, counted in characters for a
formatted file and in bytes for an unformatted one. The program below writes
three records of a name and a score and then reads record 3 before record 1,
which is the whole point of the mode.
program b4
implicit none
integer :: u
character(len=10) :: name
integer :: score
open (newunit=u, file='direct.txt', access='direct', form='formatted', &
recl=16, status='replace')
write (u, '(a10, i6)', rec=1) 'alice', 91
write (u, '(a10, i6)', rec=2) 'bob', 78
write (u, '(a10, i6)', rec=3) 'carol', 84
close (u)
open (newunit=u, file='direct.txt', access='direct', form='formatted', &
recl=16, status='old')
read (u, '(a10, i6)', rec=3) name, score
print *, 'record 3:', trim(name), score
read (u, '(a10, i6)', rec=1) name, score
print *, 'record 1:', trim(name), score
close (u)
end program b4
Each record occupies exactly sixteen characters, ten for the name and six for
the score, and rec= selects the record. Reading record 3 first
costs nothing extra, because the runtime can compute its position directly:
two records of sixteen characters come before it. The file has no line breaks,
so cat prints all three records as one line; that is normal for a
direct file, where the record boundaries are implicit in the fixed length.
$ gfortran b4.f90 -o b4
$ ./b4; cat direct.txt
record 3: carol 84
record 1: alice 91
alice 91 bob 78 carol 84
Unformatted direct access
The same mode without a format stores binary records. Here recl
is measured in bytes, so a record holding three default reals needs twelve.
This is the classic layout of a scientific data file: a header record, then one
record per row of a matrix, each of the same length. The program below writes
three rows of three reals and reads them one at a time, computing their sums.
program b5
implicit none
integer :: u, rec
real :: row(3)
open (newunit=u, file='rows.bin', access='direct', form='unformatted', &
recl=12, status='replace')
write (u, rec=1) [1.0, 2.0, 3.0]
write (u, rec=2) [4.0, 5.0, 6.0]
write (u, rec=3) [7.0, 8.0, 9.0]
close (u)
open (newunit=u, file='rows.bin', access='direct', form='unformatted', &
recl=12, status='old')
do rec = 1, 3
read (u, rec=rec) row
print *, 'row', rec, ':', row, 'sum', sum(row)
end do
close (u)
end program b5
The three sums are 6, 15 and 24, the sums of the rows of a small matrix. A
direct unformatted file has no record markers, because the length is already
known from recl; the bytes of record two start at offset twelve.
If the declared recl does not match the data written, the runtime
either pads the record or reports an error at the next read, so the length must
be chosen once and used consistently. A wrong number here is the typical reason
a binary file written on one machine cannot be read on another.
$ gfortran b5.f90 -o b5 $ ./b5 row 1 : 1.00000000 2.00000000 3.00000000 sum 6.00000000 row 2 : 4.00000000 5.00000000 6.00000000 sum 15.0000000 row 3 : 7.00000000 8.00000000 9.00000000 sum 24.0000000
A file of unknown length
When a program does not know how many values a binary file holds, it can ask for the size in bytes and divide by the size of one value. The result is the number of records or elements, which then drives the allocation. The loop below goes further and reads until the status becomes non-zero, counting the values as it goes, which is the binary equivalent of reading a text file line by line.
program b6
implicit none
integer :: u, n, ios
integer(kind=4) :: v
integer :: total, count
open (newunit=u, file='data.bin', access='stream', form='unformatted', &
status='replace')
write (u) 5, 12, 7, 30, 2
close (u)
inquire (file='data.bin', size=n)
print *, 'file size:', n, 'bytes'
total = 0
count = 0
open (newunit=u, file='data.bin', access='stream', form='unformatted', &
status='old')
do
read (u, iostat=ios) v
if (ios /= 0) exit
count = count + 1
total = total + v
end do
close (u)
print *, 'values:', count
print *, 'total: ', total
end program b6
Five integers fill twenty bytes, which is what inquire reports, and
the loop finds all five and sums them to 56. The declaration
integer(kind=4) states the width explicitly, because the size of a
value is what makes the byte count meaningful. Reading one value past the end
of a stream file returns a negative status rather than stopping the program,
which is why the loop can use the same ios /= 0 test as its text
counterpart.
$ gfortran b6.f90 -o b6 $ ./b6 file size: 20 bytes values: 5 total: 56
Changing one value in place
Stream files also support pos=, a byte offset for the next
transfer. Writing at a position overwrites the bytes there and leaves the rest
of the file alone, which is how a program can update one element of a large
array without rewriting it. Positions are counted from one, not from zero, so
the fifth integer of a stream file starts at byte 17.
program b7
implicit none
integer :: u
integer :: a(6)
a = [1, 2, 3, 4, 5, 6]
open (newunit=u, file='patch.bin', access='stream', form='unformatted', &
status='replace')
write (u) a
close (u)
open (newunit=u, file='patch.bin', access='stream', form='unformatted')
write (u, pos=4 * 4 + 1) 99
close (u)
a = 0
open (newunit=u, file='patch.bin', access='stream', form='unformatted')
read (u) a
close (u)
print *, a
end program b7
The fifth value changes from 5 to 99 and every other value stays as it was. The
offset is written as 4 * 4 + 1 to make the arithmetic visible: four
integers of four bytes each, then the first byte of the fifth. The second open
has no status=, which defaults to 'unknown': the file
must exist, and it is not emptied. This is the same default that a program uses
when it opens a data file for reading and writing without saying more.
$ gfortran b7.f90 -o b7
$ ./b7
1 2 3 4 99 6
Next steps
The Fortran files article covers the text side of
the same statements: open, close,
status and iostat. The
namelists article shows a readable format for
configuration data, and the CSV and text data
article reads a whole table into arrays. When a binary file must be exchanged
with another language, the rules for matching types are collected in the
article on C interoperability planned later in the series; until then, the
iso_fortran_env kinds such as int32 and
real64 are the safest way to state a width.
Source
Fortran 2018 standard (J3/18-007r1)
Fortran-lang: learning resources
In this article we have written unformatted sequential records and inspected
their length markers, used stream access for raw arrays and for formatted text,
reached single records with direct access in both forms, measured a file with
inquire(size=), and overwrote one value with pos=.
The previous article, Fortran files, explains the
text files that this one replaces with binary data.
Author
List all Fortran tutorials.