ZetCode

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.

b1.f90
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.

b2.f90
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.

b3.f90
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.

b4.f90
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.

b5.f90
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.

b6.f90
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.

b7.f90
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)

GNU Fortran manual

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

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Fortran tutorials.