Fortran through the standards
last modified November 14, 2026
Fortran has been standardized eight times since 1966, and each revision left the previous one almost intact. That is why a subroutine written for a 1970s mainframe can still be compiled today, and why a reader who learns the current language can also read the old code. The differences between the eras are easiest to see side by side, when one program is written once for each revision.
The program below prints the squares and the cubes of the numbers from one to five, and then the sum of the squares. It is small enough to be written in a page and rich enough to use a loop, an array, arithmetic and formatted output, which are the parts of the language that the revisions changed most often. The five versions print exactly the same six lines, and each is compiled with the flag of its own era where the compiler has one.
The task and the ground rules
The output has five numbered rows and a total: for every i from one
to five, the value, its square and its cube, and then the word
SUM followed by 55, the sum of the squares. Every version uses the
same format, I5 for the number and 2I8 for its two
powers, so the alignment is identical and a difference between the versions is a
difference in the language, not in the layout.
gfortran has no -std=f66 or -std=f77 option; the
oldest standard it knows is -std=f95. For the two oldest programs
the flag is therefore -std=legacy, which accepts everything the
compiler has ever understood, and the section for each program also shows what
a later flag does with the same source. That comparison is the practical summary
of what each revision changed.
FORTRAN 66: implicit typing and labelled loops
The oldest version declares no type for any variable. A name that begins with
I, J, K, L, M
or N is an integer and every other name is a real, so
ISQ and ISUM are integers by their first letter. The
only declaration is DIMENSION, which gives the array its size. A
loop ends at a labelled statement, the input and output formats live in
FORMAT statements that the WRITE refers to by label,
and the program ends with STOP.
DIMENSION ISQ(5)
ISUM = 0
DO 10 I = 1, 5
ISQ(I) = I*I
WRITE (6, 20) I, ISQ(I), ISQ(I)*I
ISUM = ISUM + ISQ(I)
10 CONTINUE
20 FORMAT (I5, 2I8)
WRITE (6, 30) ISUM
30 FORMAT ('SUM', I6)
STOP
END
The source is in fixed form: the label 10 sits in columns one to five, the
statement starts in column seven, and the file name ends in .f so
that the compiler reads it that way. The program has no PROGRAM
statement, because the standard of 1966 did not require one; the compiler
treats the first statement as the start of the main program. Under
-std=legacy it compiles without a word, and under
-std=f95 it also compiles silently, because nothing in it was ever
deleted from the language.
The 2018 flag is the interesting one: the labelled DO is still
legal but obsolescent, and gfortran says so. That is the whole story of Fortran
in one command, a 1966 program that still runs and a 2018 compiler that
recommends replacing one of its statements.
$ gfortran -std=legacy -Wall -Wextra squares66.f -o squares66
$ ./squares66
1 1 1
2 4 8
3 9 27
4 16 64
5 25 125
SUM 55
$ gfortran -std=f95 -Wall squares66.f -o squares66_f95
$ gfortran -std=f2018 -Wall squares66.f -o squares66_f2018
squares66.f:3:72:
3 | DO 10 I = 1, 5
| 1
Warning: Fortran 2018 obsolescent feature: Labeled DO statement at (1)
FORTRAN 77: character data and named constants
The 1977 revision added the PROGRAM statement, the
PARAMETER statement for named constants, block IF, and
the CHARACTER type, which is the first way to store text that was
not a Hollerith constant. The program now names its upper bound, stores the
label of the last line in a character variable, and writes to the unit
*, which means the default output, instead of to the number six.
PROGRAM SQUARES
INTEGER N
PARAMETER (N = 5)
INTEGER I, ISQ, ISUM
CHARACTER*3 LABEL
LABEL = 'SUM'
ISUM = 0
DO 10 I = 1, N
ISQ = I*I
WRITE (*, 20) I, ISQ, ISQ*I
ISUM = ISUM + ISQ
10 CONTINUE
20 FORMAT (I5, 2I8)
WRITE (*, 30) LABEL, ISUM
30 FORMAT (A3, I6)
END
The loop is still labelled and still ends at a CONTINUE, because
END DO did not arrive until 1990. The declaration
CHARACTER*3 is the old way of writing a length, which the next
standard replaced with CHARACTER(LEN=3) and then marked
obsolescent, so the 1995 flag warns about it while the legacy flag does not.
The warning is worth seeing rather than avoiding: it is the compiler telling a
reader which line the 1990 revision would have written differently. The output
of the program is byte for byte the output of the 1966 one, because
A3 and I6 produce the same characters as the literal
'SUM' and I6 did.
$ gfortran -std=legacy -Wall -Wextra squares77.f -o squares77
$ ./squares77
1 1 1
2 4 8
3 9 27
4 16 64
5 25 125
SUM 55
$ gfortran -std=f95 -Wall squares77.f -o squares77_f95
squares77.f:5:18:
5 | CHARACTER*3 LABEL
| 1
Warning: Obsolescent feature: Old-style character length at (1)
Fortran 90: free form, implicit none and array syntax
The 1990 revision was the largest change in the language's history. Source
lines no longer had columns, a line could be up to 132 characters and continue
with an ampersand, and the file extension became .f90.
implicit none turned an undeclared name into an error,
:: separated the attributes from the names, and END DO
replaced the label at the end of a loop. The program now declares everything,
uses PRINT with a format string, and computes the total with the
array intrinsic sum instead of adding inside the loop.
program squares90
implicit none
integer, parameter :: n = 5
integer :: i, squares(n)
do i = 1, n
squares(i) = i * i
end do
do i = 1, n
print '(i5, 2i8)', i, squares(i), squares(i) * i
end do
print '(a, i6)', 'SUM', sum(squares)
end program squares90
Fortran 90 programs compile under -std=f95 without a diagnostic,
because 95 was a small revision that added PURE,
ELEMENTAL and FORALL to what 90 had introduced. The
same standard also brought modules, array sections, allocatable arrays,
pointers, derived types and the kind mechanism, none of which this
short program needs, and every one of which a later article of this series uses.
The total is computed after the loop, from the array, which is the habit that distinguishes the modern style from the old one: the loop fills the array and the intrinsic reduces it, instead of one loop doing both jobs and hiding the reduction inside its body.
$ gfortran -std=f95 -Wall -Wextra -fcheck=all squares90.f90 -o squares90
$ ./squares90
1 1 1
2 4 8
3 9 27
4 16 64
5 25 125
SUM 55
Fortran 2003: derived types and type-bound procedures
The 2003 revision brought object orientation and interoperability with C. A
derived type can contain procedures, a dummy argument declared
class(entry) accepts the type or anything that extends it, and a
module can decide what the rest of the program is allowed to see. The program
below turns one row of the table into a type with three components and a
show method, stores the rows in an allocatable array, and calls the
method on each element.
module table_mod
implicit none
private
public :: entry, make_entry
type :: entry
integer :: base = 0
integer :: square = 0
integer :: cube = 0
contains
procedure :: show => show_entry
end type entry
contains
pure function make_entry(i) result(e)
integer, intent(in) :: i
type(entry) :: e
e%base = i
e%square = i * i
e%cube = e%square * i
end function make_entry
subroutine show_entry(this)
class(entry), intent(in) :: this
print '(i5, 2i8)', this%base, this%square, this%cube
end subroutine show_entry
end module table_mod
program squares03
use table_mod
implicit none
integer, parameter :: n = 5
type(entry), allocatable :: rows(:)
integer :: i, total
allocate (rows(n))
do i = 1, n
rows(i) = make_entry(i)
end do
total = 0
do i = 1, n
call rows(i)%show()
total = total + rows(i)%square
end do
print '(a, i6)', 'SUM', total
end program squares03
The function make_entry builds a value of the type. The module
exports the type and the constructor and keeps the subroutine private, yet the
type-bound call still reaches it, because the binding belongs to the type. The
call rows(i)%show() is a type-bound call: the compiler looks up
show in the type, finds show_entry, and passes the
element as its first argument. The same revision added allocatable components,
C interoperability, PROTECTED, stream input and output, and the
ISO_FORTRAN_ENV module.
Compiled with -std=f95 the same source is rejected, and the first
message names the construct that the older standard does not have: a
CONTAINS section inside a type definition. The second one names the
procedure binding. The run is limited to two errors so that the transcript stays
short; without the limit the compiler would go on to the class
declaration and finally complain that the module file was never written, which is
a consequence of the first error.
$ gfortran -std=f2003 -Wall -Wextra -fcheck=all squares03.f90 -o squares03
$ ./squares03
1 1 1
2 4 8
3 9 27
4 16 64
5 25 125
SUM 55
$ gfortran -std=f95 -Wall -fmax-errors=2 squares03.f90 -o squares03_f95
squares03.f90:10:12:
10 | contains
| 1
Error: Fortran 2003: CONTAINS block in derived type definition at (1)
squares03.f90:11:39:
11 | procedure :: show => show_entry
| 1
Error: Fortran 2003: PROCEDURE statement at (1)
compilation terminated due to -fmax-errors=2.
Fortran 2018: do concurrent and the implicit none list
The last version uses the two features that the previous article examined.
implicit none (type, external) requires every name and every
external procedure to be declared, and do concurrent states that
the iterations of the loop are independent, which allows a compiler to reorder
or parallelize them. The loop that fills the array is the natural place for the
construct, since each iteration writes one element and reads nothing else.
program squares18
implicit none (type, external)
integer, parameter :: n = 5
integer :: i, squares(n)
do concurrent (i = 1:n)
squares(i) = i * i
end do
do i = 1, n
print '(i5, 2i8)', i, squares(i), squares(i) * i
end do
print '(a, i6)', 'SUM', sum(squares)
end program squares18
The printing loop stays an ordinary do, because the output lines
must appear in order, and the total is again an array intrinsic. Under
-std=f2008 the compiler stops at the implicit none
list, the only construct in the program that the 2008 revision does not have;
the do concurrent itself is a 2008 feature and would be accepted. Fortran 2018 also added the assumed-rank arrays, the
collectives, the events and the error stop values described in the
previous article.
Five programs, five eras, one output. The distance between the first and the last is not only syntax: the 1966 program cannot be read without knowing the implicit typing rule, while the 2018 program states every type and every promise, and a compiler checks them.
$ gfortran -std=f2018 -Wall -Wextra -fcheck=all squares18.f90 -o squares18
$ ./squares18
1 1 1
2 4 8
3 9 27
4 16 64
5 25 125
SUM 55
$ gfortran -std=f2008 -Wall squares18.f90 -o squares18_f2008
squares18.f90:2:20:
2 | implicit none (type, external)
| 1
Error: Fortran 2018: IMPLICIT NONE with spec list at (1)
The revisions in one table
The table lists the standards in the order in which they were published, with the year of publication and the change that a reader is most likely to meet. The year in the name is not always the year of the document: Fortran 90 was published in 1991, Fortran 95 in 1997, Fortran 2003 in 2004 and Fortran 2008 in 2010, while the last two appeared in the year they are named after.
| Revision | Published | Main additions |
|---|---|---|
| FORTRAN 66 | 1966 | the first standard: fixed form, implicit typing, labelled DO, FORMAT, COMMON |
| FORTRAN 77 | 1978 | CHARACTER, block IF, PARAMETER, PROGRAM, OPEN and CLOSE |
| Fortran 90 | 1991 | free form, modules, implicit none, END DO, array syntax, allocatable arrays, derived types, kinds |
| Fortran 95 | 1997 | PURE, ELEMENTAL, FORALL, more intrinsic functions |
| Fortran 2003 | 2004 | type-bound procedures, CLASS, allocatable components, C interoperability, stream I/O |
| Fortran 2008 | 2010 | coarrays, DO CONCURRENT, submodules, ERROR STOP, the BLOCK construct |
| Fortran 2018 | 2018 | teams, collective subroutines, events, assumed rank, implicit none lists, error stop values |
| Fortran 2023 | 2023 | conditional expressions, tokenize, selected_logical_kind, reduction in do concurrent |
The table explains most of the code a reader will meet in the wild: programs
written before 1991 are in fixed form with labels, and programs written after it
are in free form with END DO. Everything else is a detail that the
compiler will point out when a flag is set. The old code is not wrong, and the
compilers still accept it, which is why the language has survived for as long as
it has.
Next steps
The what is new article takes the last two
revisions apart feature by feature and records which of them the installed
compiler accepts, and the FORTRAN 77 article
goes through the fixed-form language in detail, including the constructs that
this program did not need. The
program structure article explains the
free-form layout, the modules article covers the
unit that the 2003 version uses, and the
derived types article explains the type
with a method. The compiling and
debugging article explains the -std= flag in the context of a
build.
Source
Fortran 2018 standard (J3/18-007r1)
John Reid: The new features of Fortran 2018
GNU Fortran manual: Fortran Dialect Options
In this article we have written one small table program five times: with
implicit typing, a labelled loop and FORMAT statements in the style
of FORTRAN 66; with CHARACTER, PARAMETER and
PROGRAM in the style of FORTRAN 77; in free form with
implicit none, END DO and sum for Fortran
90; with a module, a derived type, a type-bound procedure and an allocatable
array for Fortran 2003; and with do concurrent and the
implicit none list for Fortran 2018. Every version prints the same
six lines, and the compiler messages show which flag accepts which era. The
previous article, What is new in Fortran 2018 and
2023, finishes the history with the features of the last two revisions.
Author
List all Fortran tutorials.