Fortran for Programmers of Other Languages
|
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). Every program was compiled and run on 2026-10-18 with
gfortran 16.2 using -std=f2023 -Wall -Wextra -pedantic -fimplicit-none -fcheck=all, and the conforming, runnable ones were also built with
LLVM Flang 23. The C and Python comparison programs were built with gcc 16 and run with Python 3.13 and NumPy 2.2 in the same container. The
exceptions are labelled in the text: programs that are deliberately invalid, one gfortran extension, and the MATLAB listing, which
could not be run because no MATLAB is available in the toolchain used for this page.
Fortran is the oldest high-level language still in wide use, and most of its surprises for a newcomer come from one fact: it was designed so that a compiler can prove a lot about numerical code and then optimize hard. Arrays are values, arguments are not allowed to alias, there is no pointer arithmetic, and loops over arrays can usually be written without loops. This page lists the differences that trip up programmers arriving from C, C++, Python, MATLAB, Rust and similar languages, shows each one in a small program based on the running heatflow scenario (a 2D heat-diffusion solver), and ends with tables that map familiar idioms to their Fortran counterparts. If you want the quickest orientation, read the diagram below and the tables in the last part, then come back for the explanations.
Which Fortran feature replaces the one you know
The flowchart answers the question "I would do this in my language; what do I write in Fortran?" for the most common cases. Each box is explained, with a program, in the sections that follow, or on the page named in the section.
new / delete
Vec / list"] --> B1["allocatable
(freed automatically)"] A2["pointer to an object
aliasing, linked list"] --> B2["pointer with target
(rarely needed)"] A3["struct / class / dataclass"] --> B3["derived type
type ... end type"] A4["header file
import / use / mod"] --> B4["module + use
(.mod file)"] A5["NumPy slicing
MATLAB colon ranges"] --> B5["array sections
a(2:n-1, :)"] A6["for loop over an array"] --> B6["whole-array expression
where, sum, maxval"] A7["throw / catch
Result / errno"] --> B7["iostat, stat, errmsg
error stop"] A8["threads
rayon / OpenMP"] --> B8["do concurrent
OpenMP, coarrays, MPI"] A9["unsigned int"] --> B9["wider signed kind
iand, bgt, ishft"] A10["const reference
mutable reference
pointer parameter"] --> B10["intent in, out, inout
value"]
Source: J3/23-007r1 (Fortran 2023 final draft), clauses 8.5.10 (INTENT), 9.7 (allocation), 9.5.3.4 (array sections), 11.1.7 (DO construct) and 15.5 (procedure references)
Case does not matter, and keywords are not reserved
Fortran is case insensitive: Grid_Spacing, grid_spacing and GRID_SPACING are the same name. The standard says a lower-case letter is
equivalent to the corresponding upper-case letter except in a character context such as a string literal (S 6.1.2). The usual style
today is lower case, with upper case only for named constants in some projects. Names are at most 63 characters and may contain letters,
digits and the underscore, but must start with a letter (S 6.2.2).
The words that look like keywords (if, end, real, write, print) are statement keywords, and the standard states that
a statement keyword is not a reserved word (S 5.5.2): the compiler decides from the position in the statement whether write
is a statement or a variable. That is why old programs can contain variables called end or do. It is legal but a bad idea,
and the example uses write only to prove the point.
program case_and_names
use, intrinsic :: iso_fortran_env, only: real64
implicit none
real(real64) :: Grid_Spacing
integer :: write ! legal: statement keywords are not reserved words
grid_spacing = 0.25_real64
write = 3
WRITE (*, '(A, F5.2, A, I0)') 'dx = ', GRID_SPACING, ', write = ', write
end program case_and_names
dx = 0.25, write = 3
Source: J3/23-007r1, clauses 6.1.2 (letters), 6.2.2 (names) and 5.5.2 (statement keyword)
The practical consequence for tooling is that a search for a variable must be case-insensitive, and that a formatter or a linter cannot
tell a keyword from a name by the spelling alone. The community style guide, which the later
Best Practices and Style page expands, recommends lower case for
everything and snake_case for names.
implicit none: undeclared names are not an error unless you ask
In C, Rust and Python an undeclared name is a compile-time or run-time error. In Fortran, unless the program unit says otherwise,
implicit typing is on: a name that is not declared gets a type from its first letter, i to n for default integer and everything
else for default real (S 8.7). A typo therefore silently creates a new variable. The example below is deliberately flawed (it is an
example of what not to write): it has no implicit none and misspells residual.
! Intentionally flawed: no implicit none, and a misspelled variable name.
program implicit_typing_trap
real :: residual
residual = 1.0
resdual = residual*0.5 ! typo: silently declares a new real variable
print *, 'residual =', residual
end program implicit_typing_trap
Without the strict options it compiles without a diagnostic and prints the unchanged value. With -fimplicit-none (the equivalent of
putting implicit none in every unit; the strict command used for every example on this page) it is rejected:
15_implicit_typing_trap.f90:5:4:
5 | resdual = residual*0.5 ! typo: silently declares a new real variable
| 1~~~~~~
Error: Symbol 'resdual' at (1) has no IMPLICIT type; did you mean 'residual'?
Source: J3/23-007r1, clause 8.7 (IMPLICIT statement) and GNU Fortran: Error and Warning Options
The rule for new code is: every program unit starts with implicit none (a module’s implicit none is inherited by the procedures it
contains, but a separate external procedure needs its own, which is another reason to put procedures in modules). Fortran 2018 added
implicit none (type, external), which also makes a call to a procedure without an explicit interface an error. The standard has not
removed default typing, but the work on Fortran 2028 plans to make it obsolescent, which matches the community view that
implicit none is mandatory.
Arrays count from one, or from wherever you say
A Fortran array has a lower bound and an upper bound for each dimension. The default lower bound is 1, but any integer, including zero
and negative values, can be declared (S 8.5.8). This suits numerical code: a grid with nodes numbered from zero, or a stencil array
u(-1:n+1) with ghost cells, can be written the way the mathematics reads. An array section such as u(1:3) contains both end points,
which differs from Python slices and Rust ranges, and the stride goes as the third element (u(1:9:2) is nodes 1, 3, 5, 7, 9; S 9.5.3.4.2).
A rule that surprises people: an assumed-shape dummy argument (a(:)) always has lower bound 1 in the called procedure, whatever
the bounds of the actual array were, unless the dummy declares its own lower bound (a(0:); S 8.5.8.3). The program prints
the bounds on both sides of the call.
program bounds_and_sections
use, intrinsic :: iso_fortran_env, only: real64
implicit none
real(real64) :: u(0:4) ! lower bound 0: an index per grid node
real(real64) :: v(-2:2)
integer :: i
u = [(real(i, real64), i = 0, 4)]
v = u
print '(A, 2I3)', 'lbound/ubound of u: ', lbound(u, 1), ubound(u, 1)
print '(A, 2I3)', 'lbound/ubound of v: ', lbound(v, 1), ubound(v, 1)
! Section end points are both included: u(1:3) has three elements
print '(A, 3F5.1)', 'u(1:3) = ', u(1:3)
print '(A, I0)', 'size(u(1:3)) = ', size(u(1:3))
call show(u)
call show(u(1:3))
contains
subroutine show(a)
real(real64), intent(in) :: a(:) ! assumed shape: lower bound is 1
print '(A, I0, A, I0, A, F4.1)', 'in show: lbound = ', lbound(a, 1), &
', ubound = ', ubound(a, 1), ', a(1) = ', a(1)
end subroutine show
end program bounds_and_sections
lbound/ubound of u: 0 4
lbound/ubound of v: -2 2
u(1:3) = 1.0 2.0 3.0
size(u(1:3)) = 3
in show: lbound = 1, ubound = 5, a(1) = 0.0
in show: lbound = 1, ubound = 3, a(1) = 1.0
Source: J3/23-007r1, clauses 8.5.8 (DIMENSION attribute), 8.5.8.3 (assumed-shape array) and 9.5.3.4 (array sections)
The rule matters to anyone porting loops: write loops with lbound and ubound (or do i = lbound(a, 1), ubound(a, 1)), or
use whole-array syntax, instead of assuming that the first element has index 1. NumPy and C programmers should also note that
an out-of-bounds index is not detected by default: with the plain compiler options it reads or writes neighbouring memory, exactly like C,
and only -fcheck=bounds (part of -fcheck=all) turns it into a run-time error. The section on
evaluation order shows what that message looks like.
Column-major storage
Fortran stores a multidimensional array with the first subscript varying fastest: a(1,1), a(2,1), a(1,2), a(2,2) and so on
(array element order, S 9.5.3.3). C, C++ (built-in arrays), Python lists of lists and NumPy’s default store the last subscript fastest
(row-major). MATLAB, R and Julia are column-major like Fortran. Consequences:
-
The innermost loop of a nest should run over the first index, so that consecutive iterations touch consecutive memory. A C loop nest with
ioutermost andjinnermost, ported literally toa(i, j), walks the memory with a large stride; swap the loops (or the subscripts) when porting. -
reshapefills in array element order by default, soreshape([1,2,3,4,5,6], [2,3])is the matrix with columns(1,2),(3,4)and(5,6). NumPy’sreshapefills the last index first unless you passorder="F"; Fortran needsorder=[2,1]to do what NumPy does by default. -
A whole array used where a list of values is expected, such as
print '(6I3)', aor[a], is read in array element order. Printing a matrix row by row needs an implieddoloop. -
The first subscript of a Fortran array is conceptually the row, as in mathematics (and in MATLAB), whatever the memory order is.
-
Passing a Fortran array to C code, or reading a binary file written by NumPy in its default order, needs care: the memory order is not the same unless one side transposes or reshapes. The interoperability pages return to this.
program column_major
implicit none
integer :: a(2, 3), i, j
integer :: row_major(2, 3)
! reshape fills the first subscript fastest: column-major array element order
a = reshape([1, 2, 3, 4, 5, 6], [2, 3])
print '(A)', 'a as a 2 x 3 matrix:'
do i = 1, 2
print '(3I3)', (a(i, j), j = 1, 3)
end do
print '(A, 6I3)', 'storage order (array element order): ', a
print '(A, 6I3)', 'a(1,:) = ', a(1, :)
! order=[2,1] fills the second subscript fastest, like C
row_major = reshape([1, 2, 3, 4, 5, 6], [2, 3], order=[2, 1])
print '(A)', 'filled in row order:'
do i = 1, 2
print '(3I3)', (row_major(i, j), j = 1, 3)
end do
print '(A, 6I3)', 'storage order: ', row_major
end program column_major
a as a 2 x 3 matrix:
1 3 5
2 4 6
storage order (array element order): 1 2 3 4 5 6
a(1,:) = 1 3 5
filled in row order:
1 2 3
4 5 6
storage order: 1 4 2 5 3 6
Source: J3/23-007r1, clause 9.5.3.3 (array element order) and clause 16.9.175 (RESHAPE); Fortran-lang: Python Fortran Rosetta Stone
The 2D heat grid in heatflow is stored as u(nx, ny) for that reason: the stencil loops put i (the x index) innermost. The
Performance and Memory Layout page measures the effect.
Arguments are passed by reference, and intent says what may happen
In C every argument is a copy; in Python every argument is a reference to an object that the function may mutate; in Rust ownership
decides. In Fortran, the standard speaks of argument association (S 15.5.2) instead of copying: a dummy argument becomes another
name for the actual argument, and implementations normally pass an address. The practical result is that a procedure can change its
caller’s variable, which is why every dummy argument should be given an intent:
| Declaration | Meaning | Closest equivalent |
|---|---|---|
|
The procedure may not change the argument; the compiler rejects the assignment. |
|
|
The argument is undefined on entry (allocatables are deallocated, components are default-initialized) and must be set. |
an output parameter (a pointer) in C, a |
|
Read and written. |
a pointer or reference in C and C++, |
|
The procedure gets its own copy, as in C (S 8.5.19). |
pass by value in C |
no |
Whatever the actual argument allows; avoid it. |
a plain pointer |
Source: J3/23-007r1, clauses 8.5.10 (INTENT attribute), 8.5.19 (VALUE attribute) and 15.5.2 (actual arguments, dummy arguments and argument association)
The next program shows all three behaviours: an intent(inout) argument modified in place, a pure function that returns a fresh array,
and a value argument that a procedure can change without affecting its caller.
module step_tools
use, intrinsic :: iso_fortran_env, only: real64
implicit none
private
public :: scale_in_place, scaled_copy, count_down
contains
! The dummy argument is the caller's variable, not a copy
subroutine scale_in_place(x, factor)
real(real64), intent(inout) :: x(:)
real(real64), intent(in) :: factor
x = x*factor
end subroutine scale_in_place
! A function result is a value; its arguments are intent(in)
pure function scaled_copy(x, factor) result(y)
real(real64), intent(in) :: x(:)
real(real64), intent(in) :: factor
real(real64) :: y(size(x))
y = x*factor
end function scaled_copy
! value gives C-like pass-by-value: the caller's variable is untouched
subroutine count_down(n)
integer, value :: n
do while (n > 0)
n = n - 1
end do
print '(A, I0)', 'inside count_down, n = ', n
end subroutine count_down
end module step_tools
program reference_and_intent
use, intrinsic :: iso_fortran_env, only: real64
use step_tools, only: scale_in_place, scaled_copy, count_down
implicit none
real(real64) :: t(3) = [1.0_real64, 2.0_real64, 3.0_real64]
integer :: steps
print '(A, 3F5.1)', 'scaled_copy: ', scaled_copy(t, 2.0_real64)
print '(A, 3F5.1)', 't unchanged: ', t
call scale_in_place(t, 10.0_real64)
print '(A, 3F5.1)', 'after scale_in_place: ', t
steps = 5
call count_down(steps)
print '(A, I0)', 'caller steps = ', steps
end program reference_and_intent
scaled_copy: 2.0 4.0 6.0
t unchanged: 1.0 2.0 3.0
after scale_in_place: 10.0 20.0 30.0
inside count_down, n = 0
caller steps = 5
Source: J3/23-007r1, clauses 8.5.10 (INTENT) and 8.5.19 (VALUE)
The compiler checks intent against the call. The next program is deliberately invalid (do not write this): it passes a literal
constant for an intent(inout) dummy argument. The modern compilers reject it at compile time.
! Intentionally invalid: a literal constant is passed for an intent(inout) argument.
program intent_error
implicit none
call bump(3)
contains
subroutine bump(n)
integer, intent(inout) :: n
n = n + 1
end subroutine bump
end program intent_error
05_intent_error.f90:4:13:
4 | call bump(3)
| 1
Error: Non-variable expression in variable definition context (actual argument to INTENT = OUT/INOUT) at (1)
Source: GNU Fortran: Error and Warning Options and J3/23-007r1, clause 8.5.10
No aliasing
The second rule behind the first is the one that makes Fortran fast. While a dummy argument is associated with an actual argument, the
standard requires that a part of the actual argument that is modified through one name is not accessible through any other name
during the execution of the procedure (S 15.5.2.14). C needs restrict to promise that, Rust enforces it with the borrow checker, and
Fortran simply states it as a rule of the language. A compiler can therefore keep array elements in registers and vectorize without
inserting run-time checks. The cost is that the compiler trusts the programmer: breaking the rule is not diagnosed in general,
and the program is non-conforming with an unpredictable result. Compilers detect only the simplest case, as in this deliberately
non-conforming example, which must not be used as a model:
! Intentionally NON-CONFORMING: the same variable is associated with an
! intent(in) and an intent(out) dummy argument (S 15.5.2.14). Do not write code like this.
program aliasing_nonconforming
implicit none
real :: x = 2.0
call double_into(x, x)
print '(A, F5.1)', 'x = ', x
contains
subroutine double_into(dst, src)
real, intent(out) :: dst
real, intent(in) :: src
dst = 2.0*src
end subroutine double_into
end program aliasing_nonconforming
06_aliasing_nonconforming.f90:6:20:
6 | call double_into(x, x)
| 1
Warning: Same actual argument associated with INTENT(OUT) argument 'dst' and INTENT(IN) argument 'src' at (1)
x = 4.0
Source: J3/23-007r1, clause 15.5.2.14 (restrictions on entities associated with dummy arguments)
The program printed the answer you might expect because the build is unoptimized. Another compiler, or the same compiler with another
optimization level, is free to produce something else, since the standard puts no requirement on a non-conforming program. The pointer and
target attributes, which declare that aliasing is possible, are the controlled way to opt out; see
Pointers and Targets.
Functions, subroutines and the result variable
Fortran has two kinds of procedure. A function returns a value and is used in an expression; a subroutine returns nothing and is
started with call. There is no return x: the value is whatever the result variable holds when the function ends. By default that
variable has the name of the function; the result(y) clause names it, which is required for recursion and clearer otherwise (the
scaled_copy function above). return exists, but only exits early.
-
The community habit is that a function computes a value from
intent(in)arguments without side effects (and is declaredpureorelementalso the compiler may call it in any order), while an operation with side effects or output is a subroutine. -
Several results come back through
intent(out)arguments, a derived type, or a subroutine with several outputs; there is no tuple and no multiple return values. -
The same name can refer to several procedures (a generic interface), which Fortran uses instead of overloading and default arguments. Optional and keyword arguments (
call solve(grid, tol=1.0e-6_dp)) are built into the language. -
Procedures in a module have an explicit interface, so the compiler checks the number, type, kind, rank and
intentof every argument at every call. External procedures without an interface do not get this check.
Source: J3/23-007r1, clauses 15.6.2.2 (function subprogram) and 15.6.2.3 (subroutine subprogram)
The Procedures, Functions and Subroutines page covers every attribute and form.
Whole-array expressions replace most loops
An array is a first-class value. Arithmetic, relational operators and intrinsic functions apply to whole arrays or sections, element by
element, and an assignment to an array assigns every element. A scalar is broadcast, so unew = u copies the grid and t = t*2.0
doubles every element. Intrinsic reductions (sum, maxval, count, any, all) collapse an array to a scalar. A conditional
assignment under a mask is written with where; the equivalent in NumPy is boolean indexing or np.where. A user-written
elemental function written for scalars works on arrays of any rank; and do concurrent is a loop whose iterations the programmer
declares independent, which allows the compiler to vectorize, to run threads on it, or to offload it to a GPU.
module array_demo
use, intrinsic :: iso_fortran_env, only: real64
implicit none
private
public :: clamp, relax_interior
contains
! An elemental function is written for scalars and works on arrays of any rank
elemental function clamp(x, lo, hi) result(y)
real(real64), intent(in) :: x, lo, hi
real(real64) :: y
y = min(max(x, lo), hi)
end function clamp
! do concurrent states that iterations are independent; the compiler may run them
! in any order, vectorize them or offload them
subroutine relax_interior(u, unew)
real(real64), intent(in) :: u(:, :)
real(real64), intent(out) :: unew(:, :)
integer :: i, j
unew = u
do concurrent (j = 2:size(u, 2) - 1, i = 2:size(u, 1) - 1)
unew(i, j) = 0.25_real64*(u(i + 1, j) + u(i - 1, j) + u(i, j + 1) + u(i, j - 1))
end do
end subroutine relax_interior
end module array_demo
program whole_array_and_do_concurrent
use, intrinsic :: iso_fortran_env, only: real64
use array_demo, only: clamp, relax_interior
implicit none
real(real64) :: t(6), grid(4, 4), next(4, 4)
t = [-5.0_real64, 0.0_real64, 20.0_real64, 50.0_real64, 99.0_real64, 120.0_real64]
print '(A, 6F7.1)', 'clamp: ', clamp(t, 0.0_real64, 100.0_real64)
where (t > 50.0_real64)
t = t - 50.0_real64
elsewhere
t = 0.0_real64
end where
print '(A, 6F7.1)', 'where: ', t
print '(A, F7.1, A, I0)', 'sum = ', sum(t), ' count(t > 0) = ', count(t > 0.0_real64)
grid = 0.0_real64
grid(1, :) = 100.0_real64
call relax_interior(grid, next)
print '(A, 2F7.2)', 'row 2, columns 2:3: ', next(2, 2:3)
end program whole_array_and_do_concurrent
clamp: 0.0 0.0 20.0 50.0 99.0 100.0
where: 0.0 0.0 0.0 0.0 49.0 70.0
sum = 119.0 count(t > 0) = 2
row 2, columns 2:3: 25.00 25.00
Source: J3/23-007r1, clauses 10.2.3 (masked array assignment, WHERE), 11.1.7 (DO construct, DO CONCURRENT) and 15.9 (elemental procedures)
Two details to remember. The right-hand side of an array assignment is evaluated completely before any element of the left-hand
side is changed, so a(2:n) = a(1:n-1) shifts the array by one place, with no temporary needed in the source (the compiler creates one if it
has to). And an array expression is conformable only when its operands have the same shape or one is a scalar (S 10.1.10); a mismatch
is a compile-time error, or a run-time error with -fcheck=all. The Arrays Fundamentals,
Array Intrinsics and Reductions and
do concurrent and GPU Offload pages go further.
Operators, integer division and strings
Most operators look familiar, with a few traps:
-
Integer division truncates toward zero (S 10.1.5.2.2), as in C99 and unlike Python’s
//:-7/2is-3. -
The intrinsic
mod(a, p)has the sign ofa(like C’s%) andmodulo(a, p)has the sign ofp(like Python’s%) (S 16.9.145 and 16.9.146). -
Exponentiation is
**and is right-associative, so2**3**2is512. There is no^operator, no increment or decrement operator, and no compound assignment such asx += 1. -
Equality is
==and inequality/=; the older spellings.eq.and.ne.still work. Logical operators are.and.,.or.,.not.,.eqv.and.neqv.. -
A string has a fixed length chosen at declaration, shorter values are padded with blanks, and the relational operators compare as if the shorter operand were padded with blanks, so
label == 'u'is true even thoughlabelholdsufollowed by seven blanks (S 10.1.5.5.2). Usetrimorlen_trimfor the content, and acharacter(len=:), allocatablevariable for a string that should have the exact length of its value. -
Concatenation is
//, quotes are single or double (a doubled quote inside a literal is a quote), and there are no escape sequences. -
Arithmetic with mixed types converts to the "higher" type (
realoverinteger), soreal(n)/2andn/2.0differ fromn/2.
program operators_and_strings
implicit none
integer :: n
real :: x
character(len=8) :: label
character(len=:), allocatable :: name
n = 7
print '(A, I0, 2A, I0)', '7/2 = ', n/2, ' ', '-7/2 = ', (-n)/2 ! truncates toward zero
print '(A, I0, A, I0)', 'mod(-7,3) = ', mod(-n, 3), ' modulo(-7,3) = ', modulo(-n, 3)
x = real(n)/2
print '(A, F4.1)', 'real(7)/2 = ', x
print '(A, I0, A, F6.2)', '2**3**2 = ', 2**3**2, ' (-2.0)**2 = ', (-2.0)**2
print '(A, L1)', '1 /= 2 is ', 1 /= 2
! Fixed-length strings are blank padded; == ignores trailing blanks
label = 'u'
print '(3A, I0)', '[', label, '] len = ', len(label)
print '(A, L1, A, I0)', "label == 'u': ", label == 'u', ' len_trim = ', len_trim(label)
name = 'heatflow' ! deferred-length: sized by assignment
name = name // '_cli'
print '(2A, A, I0)', name, ' ', 'len = ', len(name)
print '(A)', 'He said "hello" and ''left'''
end program operators_and_strings
7/2 = 3 -7/2 = -3
mod(-7,3) = -1 modulo(-7,3) = 2
real(7)/2 = 3.5
2**3**2 = 512 (-2.0)**2 = 4.00
1 /= 2 is T
[u ] len = 8
label == 'u': T len_trim = 1
heatflow_cli len = 12
He said "hello" and 'left'
Source: J3/23-007r1, clauses 10.1.3 (precedence), 10.1.5.2.2 (integer division), 10.1.5.5.2 (relational operations on characters) and 16.9.145 and 16.9.146 (MOD and MODULO)
No short-circuit evaluation
a .and. b does not promise that b is skipped when a is false, and a .or. b does not promise to skip b when a is true. The standard
says that a processor need not evaluate all operands of an expression if the value can be determined otherwise (S 10.1.7); it
does not say it must not, and the choice may even change between optimization levels. A C idiom such as i < n && a[i] != 0 therefore has no safe Fortran
spelling in a single logical expression. The second loop below is deliberately unsafe: it relies on i <= n being tested first. With gfortran
and -fcheck=all both operands were evaluated and the out-of-bounds read was reported; with Flang 23, which does no bounds checking by default,
the same program silently read past the array and printed unsafe search finished at 6. The first loop, with the test and the access in
separate statements, is the portable form. The text block shows the gfortran run, with the backtrace removed.
! The first loop is intentionally NON-CONFORMING in spirit: .and. does not
! guarantee that a(i) is skipped when i > n (S 10.1.7). It is run to show what
! -fcheck=all reports; the second loop is the portable form.
program no_short_circuit
implicit none
integer, parameter :: n = 5
integer :: a(n) = [3, 1, 4, 1, 5]
integer :: i
i = 1
do while (i <= n)
if (a(i) == 4) exit
i = i + 1
end do
print '(A, I0)', 'safe search, nested tests: found at ', i
i = 1
do while (i <= n .and. a(i) /= 9) ! may evaluate a(6)
i = i + 1
end do
print '(A, I0)', 'unsafe search finished at ', i
end program no_short_circuit
safe search, nested tests: found at 3
At line 18 of file 07_no_short_circuit.f90
Fortran runtime error: Index '6' of dimension 1 of array 'a' above upper bound of 5
Source: J3/23-007r1, clause 10.1.7 (evaluation of operands) and GNU Fortran: Options for code generation conventions (-fcheck)
The same sentence of the standard also says that if a part of an expression is not evaluated, a function reference in it that would have modified its arguments leaves them undefined. This is another argument for the "functions have no side effects" rule above.
No unsigned integers
Fortran 2023 has signed integers only. The integer type is defined by a model with a sign (S 7.4.3.1), and the kinds
int8, int16, int32 and int64 from iso_fortran_env are all signed. Hash functions, checksums, random-number generators and
file formats that need uint32 or uint64 arithmetic need one of three workarounds:
-
Mask in a wider signed kind: keep a 32-bit value in an
int64and applyiand(x, mask)after each step, as in the first half of the program below. For a full 64-bit value there is no wider kind, so use the bit-pattern tools of the next point. -
Compare bit patterns: the bitwise comparison intrinsics
bge,bgt,bleandblt(S 16.9) treat their arguments as unsigned bit strings, andishft,ishftc,shiftl,shiftr,iand,ior,ieor,maskandpopcntoperate on the bit pattern.transferreinterprets the bits. -
Ask the compiler for the extension: gfortran accepts an experimental
unsignedtype when-funsignedis given.
The standard does not define what happens when a signed integer overflows, so relying on the wrap-around of a signed kind is a bug even though common compilers wrap at run time; do not use it to emulate unsigned arithmetic.
program unsigned_in_standard_fortran
use, intrinsic :: iso_fortran_env, only: int32, int64
implicit none
integer(int64), parameter :: mask32 = int(z'FFFFFFFF', int64)
integer(int64) :: counter
integer(int32) :: a, b
! Option 1: keep the value in a wider signed kind and mask after each step
counter = 0_int64
counter = iand(counter - 1_int64, mask32) ! wraps like uint32
print '(A, I0)', 'masked 0 - 1 = ', counter
! Option 2: signed storage, unsigned interpretation through bit comparison
a = -1_int32 ! bit pattern 0xFFFFFFFF
b = 1_int32
print '(A, L1, A, L1)', 'a < b (signed): ', a < b, ' bgt(a, b) (unsigned): ', bgt(a, b)
print '(A, I0)', 'a as unsigned: ', iand(int(a, int64), mask32)
end program unsigned_in_standard_fortran
masked 0 - 1 = 4294967295
a < b (signed): T bgt(a, b) (unsigned): T
a as unsigned: 4294967295
Source: J3/23-007r1, clauses 7.4.3.1 (integer type), 16.9.46 (BGT) and 16.9.100 (IAND)
The extension, for comparison, is not standard Fortran 2023: it needs -funsigned, it is rejected under -std=f2023, and
the GNU Fortran manual describes it as following a J3 paper (J3/24-116) rather than the standard. It provides a type unsigned
with kinds selected by selected_unsigned_kind, a u literal suffix, modular arithmetic, explicit conversion with int and uint,
and no use as do index or array subscript. The manual says the implementation is compatible with Flang, and the
same source built with Flang 23 and -funsigned in the check made for this page. The next program is the extension example, compiled with
gfortran -funsigned -Wall -Wextra -fimplicit-none -fcheck=all, not with -std=f2023.
! gfortran extension, NOT standard Fortran 2023: compile with -funsigned (no -std=f2023).
program unsigned_extension
use, intrinsic :: iso_fortran_env, only: uint32
implicit none
unsigned(kind=uint32) :: counter
integer :: shown
counter = 0u_uint32
counter = counter - 1u_uint32 ! wraps modulo 2**32
print '(A, I0)', 'unsigned 0 - 1 = ', counter
shown = int(counter/65536u_uint32)
print '(A, I0)', 'high half = ', shown
end program unsigned_extension
unsigned 0 - 1 = 4294967295
high half = 65535
Source: GNU Fortran: Unsigned integers and GNU Fortran: Options controlling Fortran dialect (-funsigned)
No exceptions
Fortran has no try/catch/throw and no Result type. Errors are reported in one of four ways:
-
Status arguments. Statements that can fail take optional
iostat=(an integer, zero on success, negative at end of file) andiomsg=for I/O, andstat=anderrmsg=forallocate,deallocateand the coarray statements. If you give no status variable and the operation fails, the program terminates. The codes are processor-dependent, so testios /= 0or compare withiostat_endandiostat_eorfromiso_fortran_env, never against a literal like 2. -
err=,end=andeor=branch labels. Older code; they are not obsolescent in the standard, butiostat=is the style used today. -
Termination.
stopends the program;error stopends all images in error termination with a nonzero exit status (S 11.4). There is no unwinding and no stack trace in the language; the gfortran runtime prints a backtrace on error termination, as the output below shows. -
Floating-point exceptions are flags, not control flow. By default gfortran and Flang do not trap, so
1.0/0.0is an IEEE infinity and0.0/0.0a NaN. Theieee_arithmeticandieee_exceptionsmodules test and set the flags; a compiler option such as-ffpe-trapturns them into signals.
program no_exceptions
use, intrinsic :: iso_fortran_env, only: real64, int64
use, intrinsic :: ieee_arithmetic, only: ieee_is_finite, ieee_is_nan
implicit none
integer :: unit, ios, stat
character(len=256) :: msg
real(real64), allocatable :: big(:)
real(real64) :: zero, ratio
integer(int64) :: huge_count
! 1. I/O: failures come back as a status code and a message
open (newunit=unit, file='no-such-directory/heatflow.nml', status='old', &
action='read', iostat=ios, iomsg=msg)
if (ios /= 0) print '(A, I0, 2A)', 'open failed, iostat = ', ios, ': ', trim(msg)
! 2. Allocation: stat= instead of an exception
huge_count = 2_int64**59
allocate (big(huge_count), stat=stat, errmsg=msg)
if (stat /= 0) print '(A, I0, 2A)', 'allocate failed, stat = ', stat, ': ', trim(msg)
! 3. Floating point: by default no trap; the result is an IEEE value
zero = 0.0_real64
ratio = 1.0_real64/zero
print '(A, L1, A, L1)', 'finite: ', ieee_is_finite(ratio), ', nan: ', ieee_is_nan(ratio)
ratio = zero/zero
print '(A, L1)', '0/0 is nan: ', ieee_is_nan(ratio)
! 4. Unrecoverable: terminate all images with a nonzero exit status
error stop 'giving up: no configuration'
end program no_exceptions
open failed, iostat = 2: Cannot open file 'no-such-directory/heatflow.nml': No such file or directory
allocate failed, stat = 5020: Insufficient virtual memory
finite: F, nan: F
0/0 is nan: T
Note: The following floating-point exceptions are signalling: IEEE_INVALID_FLAG IEEE_DIVIDE_BY_ZERO
ERROR STOP giving up: no configuration
Source: J3/23-007r1, clauses 9.7.4 (STAT= specifier), 11.4 (STOP and ERROR STOP statements) and 12.11.5 (IOSTAT= specifier); GNU Fortran: Runtime options
The Note: about the floating-point flags and the ERROR STOP line are written to standard error, the other lines to standard output,
and the process exit status was 1. The exact iostat value and the message text
come from the runtime library: the same program built with Flang 23 prints iostat = 2 with a shorter message, and a different stat
value for the failed allocation.
The Error Handling and Program Termination and Floating-Point and IEEE Arithmetic pages cover this topic in full.
Memory: allocatable, pointer and derived types
Dynamic memory is rarely managed by hand. A variable with the allocatable attribute owns its storage: allocate creates it, assigning
to it resizes it to match the right-hand side, and it is released automatically when the variable goes out of scope, including its
allocatable components. There is no leak and no double free, and copying a derived type with allocatable components makes a deep copy. A
pointer is a different thing: an alias that can be re-associated with =>, to a target that was declared target or allocated
through the pointer. It is the tool for linked structures or views, not the default for arrays. A derived type groups components like a
struct, and may have type-bound procedures and inheritance.
module field_types
use, intrinsic :: iso_fortran_env, only: real64
implicit none
private
public :: field_t, make_field
! The Fortran counterpart of a C struct holding a malloc'd array
type :: field_t
integer :: nx = 0
integer :: ny = 0
real(real64), allocatable :: u(:, :)
end type field_t
contains
function make_field(nx, ny) result(f)
integer, intent(in) :: nx, ny
type(field_t) :: f
f%nx = nx
f%ny = ny
allocate (f%u(nx, ny), source=0.0_real64)
end function make_field
end module field_types
program pointers_allocatables_types
use, intrinsic :: iso_fortran_env, only: real64
use field_types, only: field_t, make_field
implicit none
type(field_t) :: a, b
real(real64), pointer :: row_view(:)
real(real64), allocatable, target :: line(:)
a = make_field(3, 2)
a%u(2, 1) = 7.0_real64
b = a ! deep copy: allocatable components are copied
b%u(2, 1) = -1.0_real64
print '(A, 2F6.1)', 'a%u(2,1), b%u(2,1) = ', a%u(2, 1), b%u(2, 1)
! Assignment to an allocatable reallocates it to the shape of the right-hand side
line = a%u(:, 1)
print '(A, I0)', 'size(line) = ', size(line)
line = [line, 9.0_real64]
print '(A, I0)', 'size(line) after append = ', size(line)
! A pointer is an alias, never a copy; => associates it
row_view => line(2:3)
row_view(1) = 100.0_real64
print '(A, 4F7.1)', 'line = ', line
print '(A, L1)', 'associated(row_view) = ', associated(row_view)
nullify (row_view)
print '(A, L1)', 'after nullify = ', associated(row_view)
! a, b and line are freed automatically when the program unit ends
end program pointers_allocatables_types
a%u(2,1), b%u(2,1) = 7.0 -1.0
size(line) = 3
size(line) after append = 4
line = 0.0 100.0 0.0 9.0
associated(row_view) = T
after nullify = F
Source: J3/23-007r1, clauses 7.5 (derived types), 8.5.3 (ALLOCATABLE attribute), 8.5.14 (POINTER attribute), 9.7.1 (ALLOCATE statement) and 10.2.1.3 (interpretation of intrinsic assignments)
Side-by-side maps to other languages
From C and C++
| C and C++ | Fortran | Notes |
|---|---|---|
|
|
Freed automatically; |
|
|
Only needed for aliasing; most code uses |
|
|
Allocatable components are deep-copied and freed with the object. |
|
|
A module compiles to a |
|
|
Size travels with the array; |
|
|
Checked at compile time. |
pass by value |
|
Needed for |
|
the default |
Aliasing rule of S 15.5.2.14. |
function pointer |
|
Abstract interfaces describe the signature. |
|
|
Constants of integer kind; F2023 adds |
|
|
|
|
|
Format strings use edit descriptors. |
|
|
|
|
|
Not a global. |
|
|
A surprise of its own: |
|
|
The loop bound is evaluated once; modifying |
|
|
Comments to a compiler without OpenMP. |
Source: J3/23-007r1, clauses 8.5.16 (SAVE attribute), 18 (interoperability with C) and 15.5.2.14; Fortran-lang: Derived types
The C reference pages are Pointers, Arrays and Strings, Structures, Unions and Type Aliases and Threads; the C++ reference has Memory Management and Smart Pointers. Calling C from Fortran and Fortran from C is the topic of Interoperability with C.
From Python and NumPy
The community’s Python Fortran Rosetta Stone places NumPy and Fortran idioms side by side. The headline of that page is that the two languages are very similar in expressiveness for array code, and the points below are the exceptions and the idioms.
| Python and NumPy | Fortran | Notes |
|---|---|---|
|
|
Default lower bound is 1; indices use parentheses. |
|
|
NumPy excludes the end, Fortran includes it; both give three elements. |
|
|
Stride is the third element. |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Memory order differs (see above). |
|
|
Assignment of an array copies it. |
|
|
Contiguous in Fortran, strided in C order. |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Kinds, from |
|
|
The standard library is small; add packages with fpm. |
f2py |
|
The Python reference pages that help here are Functions and Modules and Packages.
From MATLAB
MATLAB and Fortran share the array model: one-based, column-major, whole-array syntax, end-delimited blocks. MATLAB programmers find
the array code almost identical and the surrounding language more verbose.
| MATLAB | Fortran | Notes |
|---|---|---|
|
|
Fortran has no |
|
|
The operators differ: |
|
|
|
|
|
Declaration and allocation are separate steps. |
|
|
|
|
|
|
variables appear when assigned |
every variable is declared |
|
|
|
Or a function returning a derived type. |
|
|
|
|
|
Literals need a kind suffix for non-default precision; a plain |
|
|
From Rust
| Rust | Fortran | Notes |
|---|---|---|
ownership, drop at end of scope |
|
The owner is the variable; there is no move, assignment copies. |
|
|
Fortran states the rule (no aliasing) but the compiler does not enforce it. |
|
|
Assignment to a larger right-hand side reallocates, so |
|
|
|
|
|
|
|
|
|
|
|
|
trait |
abstract type with deferred type-bound procedures |
|
generics |
generic interfaces; templates arrive with Fortran 2028 |
|
crate, |
module, submodule |
|
|
|
Source: J3/23-007r1, clauses 8.5.3, 8.5.10 and 15.5.2.14
The Rust references are Ownership and Moves, Borrowing and References, Data Parallelism with Rayon and FFI and C Interop.
The same kernel in four languages
The heat-diffusion kernel of heatflow is one explicit step of the five-point stencil with a coefficient r of 0.2. The input is a
5 by 5 grid of zeros with 100 in the middle; the result and the residual (the largest change) are printed. The first program is Fortran,
a module with one subroutine written with array sections, and it produces the same numbers as the C, Python and MATLAB versions.
module stencil_demo
use, intrinsic :: iso_fortran_env, only: real64
implicit none
private
public :: ftcs_step
contains
subroutine ftcs_step(u, r, unew, residual)
real(real64), intent(in) :: u(:, :)
real(real64), intent(in) :: r ! alpha*dt/dx**2 (dx = dy)
real(real64), intent(out) :: unew(:, :)
real(real64), intent(out) :: residual
integer :: nx, ny
nx = size(u, 1)
ny = size(u, 2)
unew = u ! boundary values carry over
unew(2:nx - 1, 2:ny - 1) = u(2:nx - 1, 2:ny - 1) + r*( &
u(3:nx, 2:ny - 1) + u(1:nx - 2, 2:ny - 1) + &
u(2:nx - 1, 3:ny) + u(2:nx - 1, 1:ny - 2) - &
4.0_real64*u(2:nx - 1, 2:ny - 1))
residual = maxval(abs(unew - u))
end subroutine ftcs_step
end module stencil_demo
program stencil
use, intrinsic :: iso_fortran_env, only: real64
use stencil_demo, only: ftcs_step
implicit none
real(real64) :: u(5, 5), unew(5, 5), residual
u = 0.0_real64
u(3, 3) = 100.0_real64
call ftcs_step(u, 0.2_real64, unew, residual)
print '(A, F8.3, A, F8.3)', 'residual = ', residual, ' center = ', unew(3, 3)
print '(A, F8.3)', 'neighbour = ', unew(3, 4)
end program stencil
residual = 80.000 center = 20.000
neighbour = 20.000
Source: Fortran-lang: Python Fortran Rosetta Stone and J3/23-007r1, clause 9.5.3.4 (array sections)
The C version has explicit loops, the index order of a row-major array, and a loop to compute the residual. It was built with
gcc -std=c17 -Wall -Wextra -pedantic and printed the same two lines.
#include <math.h>
#include <stdio.h>
#define NX 5
#define NY 5
/* u[i][j] with j the fastest index: C order, so the loop nest is i-outer */
static double ftcs_step(double u[NX][NY], double r, double unew[NX][NY]) {
double residual = 0.0;
for (int i = 0; i < NX; ++i)
for (int j = 0; j < NY; ++j)
unew[i][j] = u[i][j];
for (int i = 1; i < NX - 1; ++i) {
for (int j = 1; j < NY - 1; ++j) {
unew[i][j] = u[i][j] + r * (u[i + 1][j] + u[i - 1][j] +
u[i][j + 1] + u[i][j - 1] - 4.0 * u[i][j]);
double d = fabs(unew[i][j] - u[i][j]);
if (d > residual) residual = d;
}
}
return residual;
}
int main(void) {
double u[NX][NY] = {{0}}, unew[NX][NY];
u[2][2] = 100.0;
double residual = ftcs_step(u, 0.2, unew);
printf("residual = %8.3f center = %8.3f\n", residual, unew[2][2]);
printf("neighbour = %8.3f\n", unew[2][3]);
return 0;
}
residual = 80.000 center = 20.000
neighbour = 20.000
Source: cppreference: Array declaration
The NumPy version is the closest one to the Fortran code, apart from the exclusive slice ends and the zero-based indices.
import numpy as np
def ftcs_step(u, r):
unew = u.copy()
unew[1:-1, 1:-1] = u[1:-1, 1:-1] + r * (
u[2:, 1:-1] + u[:-2, 1:-1] + u[1:-1, 2:] + u[1:-1, :-2] - 4.0 * u[1:-1, 1:-1]
)
return unew, np.max(np.abs(unew - u))
u = np.zeros((5, 5))
u[2, 2] = 100.0
unew, residual = ftcs_step(u, 0.2)
print(f"residual = {residual:8.3f} center = {unew[2, 2]:8.3f}")
print(f"neighbour = {unew[2, 3]:8.3f}")
residual = 80.000 center = 20.000
neighbour = 20.000
The MATLAB version is the closest of all: only end replaces the explicit bounds. This listing was not run, because no MATLAB is
available in the toolchain used for this page; it follows the syntax documented for R2026b (the 'all' option of max needs MATLAB R2018b
or later).
function [unew, residual] = ftcs_step(u, r)
% Not run: no MATLAB is available in the toolchain used for this page.
unew = u;
unew(2:end-1, 2:end-1) = u(2:end-1, 2:end-1) + r * ( ...
u(3:end, 2:end-1) + u(1:end-2, 2:end-1) + ...
u(2:end-1, 3:end) + u(2:end-1, 1:end-2) - 4 * u(2:end-1, 2:end-1));
residual = max(abs(unew - u), [], 'all');
end
Source: MathWorks: Fortran API (MATLAB) for the array model shared with Fortran; the listing itself is unverified, as stated above
Where the books are outdated
Two books informed the concept coverage of this section. The Fortran book (Modern Fortran Explained: Incorporating Fortran 2023) is current for the language, and its authors' list of deprecated features is a set of recommendations, not the standard’s lists of obsolescent and deleted features; the appendix is a good guide to style but not to what a compiler accepts. This page differs from a book written for a particular compiler release in these points:
| Topic | Older text | Current state |
|---|---|---|
Implicit typing |
Older tutorials, and C programmers' habit, accept default typing. |
Use |
Unsigned integers |
Books and tutorials say "Fortran has no unsigned type", correctly for the standard. |
Still true for Fortran 2023. GNU Fortran offers the experimental |
Source form |
Old texts use fixed form, |
Free form with kind parameters ( |
Compiler support |
Books give no support table and name old compiler releases. |
Check the table on History, Standards and Compilers. |
Python and Fortran |
f2py examples use |
NumPy 2.x uses Meson as the only f2py backend. |
MATLAB |
The MATLAB API reference (R2023a) uses fixed-form |
Use a free-form |
Where to go next
-
Getting Started to install a compiler and run a first program.
-
Program Structure and Source Form and Intrinsic Types and Kinds for the basics.
-
Common Gotchas and Pitfalls for the longer list of traps.
-
Fortran Reference for the table of contents and the bibliography.
References
-
J3/23-007r1: Fortran 2023 final draft (clauses 5.5.2, 6.1.2, 6.2.2, 7.4.3.1, 8.5, 8.7, 9.5.3, 9.7, 10.1, 11.1.7, 11.4, 12.11, 15.5, 15.6, 15.9 and 16.9)