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.

flowchart LR A1["malloc / free
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 i outermost and j innermost, ported literally to a(i, j), walks the memory with a large stride; swap the loops (or the subscripts) when porting.

  • reshape fills in array element order by default, so reshape([1,2,3,4,5,6], [2,3]) is the matrix with columns (1,2), (3,4) and (5,6). NumPy’s reshape fills the last index first unless you pass order="F"; Fortran needs order=[2,1] to do what NumPy does by default.

  • A whole array used where a list of values is expected, such as print '(6I3)', a or [a], is read in array element order. Printing a matrix row by row needs an implied do loop.

  • 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

intent(in)

The procedure may not change the argument; the compiler rejects the assignment.

const T& in C++, &T in Rust

intent(out)

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 &mut T that is only written to

intent(inout)

Read and written.

a pointer or reference in C and C++, &mut T in Rust

value

The procedure gets its own copy, as in C (S 8.5.19).

pass by value in C

no intent

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)

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 declared pure or elemental so 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 intent of 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/2 is -3.

  • The intrinsic mod(a, p) has the sign of a (like C’s %) and modulo(a, p) has the sign of p (like Python’s %) (S 16.9.145 and 16.9.146).

  • Exponentiation is ** and is right-associative, so 2**3**2 is 512. There is no ^ operator, no increment or decrement operator, and no compound assignment such as x += 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 though label holds u followed by seven blanks (S 10.1.5.5.2). Use trim or len_trim for the content, and a character(len=:), allocatable variable 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 (real over integer), so real(n)/2 and n/2.0 differ from n/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:

  1. Mask in a wider signed kind: keep a 32-bit value in an int64 and apply iand(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.

  2. Compare bit patterns: the bitwise comparison intrinsics bge, bgt, ble and blt (S 16.9) treat their arguments as unsigned bit strings, and ishft, ishftc, shiftl, shiftr, iand, ior, ieor, mask and popcnt operate on the bit pattern. transfer reinterprets the bits.

  3. Ask the compiler for the extension: gfortran accepts an experimental unsigned type when -funsigned is 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

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) and iomsg= for I/O, and stat= and errmsg= for allocate, deallocate and the coarray statements. If you give no status variable and the operation fails, the program terminates. The codes are processor-dependent, so test ios /= 0 or compare with iostat_end and iostat_eor from iso_fortran_env, never against a literal like 2.

  • err=, end= and eor= branch labels. Older code; they are not obsolescent in the standard, but iostat= is the style used today.

  • Termination. stop ends the program; error stop ends 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.0 is an IEEE infinity and 0.0/0.0 a NaN. The ieee_arithmetic and ieee_exceptions modules test and set the flags; a compiler option such as -ffpe-trap turns 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.

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

double *p = malloc(n * sizeof *p)

real(dp), allocatable :: p(:) and allocate(p(n))

Freed automatically; stat= and errmsg= instead of testing for a null pointer.

double *p aliasing an array

real(dp), pointer :: p(:) with target on the array

Only needed for aliasing; most code uses allocatable and assumed-shape dummies.

struct grid { int nx; double *u; }

type :: grid_t with integer :: nx and real(dp), allocatable :: u(:,:)

Allocatable components are deep-copied and freed with the object.

#include "grid.h"

use heatflow_grid

A module compiles to a .mod file; no header is written by hand. Dependent files must be compiled after the module.

void f(double *x, int n)

subroutine f(x) with real(dp), intent(inout) :: x(:)

Size travels with the array; size(x) replaces n.

const double *

intent(in)

Checked at compile time.

pass by value

value attribute

Needed for bind(c) interfaces.

restrict

the default

Aliasing rule of S 15.5.2.14.

function pointer

procedure(interface), pointer

Abstract interfaces describe the signature.

enum

enum, bind(c) with enumerator ::

Constants of integer kind; F2023 adds enumeration type, not yet accepted by gfortran 16.2 or Flang 23.

bool

logical; logical(c_bool) across the language boundary

.true. and .false.

printf("%8.3f\n", x)

print '(F8.3)', x

Format strings use edit descriptors.

argc, argv

command_argument_count(), get_command_argument

errno

iostat=, stat=

Not a global.

static local variable

save attribute (implied by initialization in the declaration)

A surprise of its own: integer :: n = 0 in a procedure makes n saved.

for (i = 0; i < n; ++i)

do i = 1, n

The loop bound is evaluated once; modifying i inside the loop is not allowed.

#pragma omp parallel for

!$omp parallel do, do concurrent

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

a[0]

a(1)

Default lower bound is 1; indices use parentheses.

a[1:4]

a(2:4)

NumPy excludes the end, Fortran includes it; both give three elements.

a[::2]

a(1::2) or a(1:n:2)

Stride is the third element.

a[a > 0]

pack(a, a > 0), where (a > 0)

pack returns a rank-1 array; where assigns under a mask.

a.shape, a.size, a.ndim

shape(a), size(a), rank(a)

a.sum(axis=0)

sum(a, dim=1)

dim counts from 1; NumPy axis=0 and Fortran dim=1 both collapse the first index.

a @ b

matmul(a, b)

* is element-wise.

np.zerosn, m

allocate(a(n, m), source=0.0_dp) or a = 0.0_dp

a.T

transpose(a)

reshape(a, (2, 3))

reshape(a, [2, 3], order=[2, 1])

Memory order differs (see above).

a.copy()

b = a

Assignment of an array copies it.

a[:, 0]

a(:, 1)

Contiguous in Fortran, strided in C order.

np.where(c, x, y)

merge(x, y, c)

for i in range(n):

do i = 1, n

None, optional argument

optional dummy argument and present(x)

ValueError, try

iostat, stat, error stop

int, float

integer(int32), real(real64)

Kinds, from iso_fortran_env.

import numpy

use stdlib_linalg (from the Fortran stdlib)

The standard library is small; add packages with fpm.

f2py

python -m numpy.f2py, with Meson as the only build backend in NumPy 2.5

See Calling Fortran from C, C++ and Python.

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

A(2:end-1, 2:end-1)

A(2:nx-1, 2:ny-1)

Fortran has no end inside an index; use ubound or size, or a named constant.

A .* B, A * B

A * B, matmul(A, B)

The operators differ: * is element-wise in Fortran and matmul is the matrix product.

A'

transpose(A)

zeros(n, m)

a = 0.0_dp after allocate(a(n, m))

Declaration and allocation are separate steps.

numel(A), size(A, 1)

size(A), size(A, 1)

A(A > 0)

pack(A, A > 0)

variables appear when assigned

every variable is declared

implicit none.

function [y, r] = f(x)

subroutine f(x, y, r) with intent(out) for y and r

Or a function returning a derived type.

fprintf('%8.3f\n', x)

print '(F8.3)', x

1e-6

1.0e-6_dp

Literals need a kind suffix for non-default precision; a plain 1.0e-6 is single precision.

mex, mexFunction

bind(c) gateway

See MATLAB: MEX, Engine and MAT-File APIs.

From Rust

Rust Fortran Notes

ownership, drop at end of scope

allocatable

The owner is the variable; there is no move, assignment copies.

&T, &mut T

intent(in), intent(inout)

Fortran states the rule (no aliasing) but the compiler does not enforce it.

Vec<f64>

real(dp), allocatable :: v(:)

Assignment to a larger right-hand side reallocates, so v = [v, x] appends.

Option<T>

optional and present, or allocated/associated

Result<T, E>

stat, iostat, errmsg

panic!

error stop

unsafe pointers

pointer and target; c_ptr for C

trait

abstract type with deferred type-bound procedures

generics

generic interfaces; templates arrive with Fortran 2028

crate, mod

module, submodule

rayon

do concurrent, OpenMP

Source: J3/23-007r1, clauses 8.5.3, 8.5.10 and 15.5.2.14

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

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 implicit none in every unit, or -fimplicit-none in the build. Fortran 2028 plans to make default implicit typing obsolescent.

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 -funsigned extension, which Flang also accepts; neither is standard.

Source form

Old texts use fixed form, real*8 and double precision.

Free form with kind parameters (real(real64)); see Legacy Features and Modernization for old code you may meet.

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 distutils.

NumPy 2.x uses Meson as the only f2py backend.

MATLAB

The MATLAB API reference (R2023a) uses fixed-form .F gateways, integer*4 and real*8.

Use a free-form .F90 gateway with kinds from iso_fortran_env; see the MEX page.

Where to go next