Best Practices and Style
|
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) and was checked on 2026-10-19. Every
example was compiled with gfortran -std=f2023 -Wall -Wextra -pedantic -fimplicit-none -fcheck=all (GNU Fortran 16.2)
and run, and the examples that are portable were also built and run with LLVM Flang 23; the output and the diagnostics
shown are real. Examples that are deliberately wrong say so and show the real message of the compiler that printed it.
Nothing was tested with Intel ifx, NAG, HPE Cray or NVIDIA HPC compilers, which are not installed in the verification
container.
Fortran has no official style guide. The standard defines what is legal; what is good comes from the community. The
two written sources used here are the fortran-lang Fortran Style Guide and the other pages of its Best Practices
section (which cover naming and indentation, and then floating point, integer division, modules and programs, arrays,
allocatable arrays, file input/output and callbacks), and the style guide of the Fortran standard library, stdlib,
which adds rules about line length, intent, end statements, file names and documentation. Both are community
documents and both were read on 2026-10-19; neither is part of the language. Each rule below is given together with the
reason for it and with a pointer to the page where the underlying feature is explained, so that the advice is not taken on
faith. The final section sets the deprecation list printed in an appendix of one of the reference books against the
standard’s own list of obsolescent features, because the two are different things.
The running example is heatflow, a 2D heat-diffusion fpm project whose modules (heatflow_kinds, heatflow_constants,
heatflow_boundary, heatflow_grid, heatflow_kernels, heatflow_solver, heatflow_config, heatflow_io and
heatflow_capi) were written to follow the rules on this page. The page is a summary that points into the rest of the
guide: the pitfalls are collected in Common Gotchas and Pitfalls, speed in
Performance and Memory Layout, and the old constructs a modern program should not
contain in Legacy Features and Modernization.
The rules at a glance
| Practice | Why | Details |
|---|---|---|
Write |
a misspelled name becomes a compile-time error instead of a silent new variable; a call to an undeclared external procedure is rejected |
|
Define kinds once in a module, write literals with the kind suffix |
portable precision; no default-real surprises |
|
Give every dummy argument an |
documents the data flow and lets the compiler reject writes to inputs |
Intent, pure and elemental, Procedures, Functions and Subroutines |
Mark procedures |
no side effects, usable in |
|
Put every procedure in a module |
explicit interfaces for free, no hand-written interface blocks |
|
Make modules |
the interface is small and deliberate; internals can change |
|
Use |
every name is traceable to its module; no accidental clashes |
|
Prefer |
automatic deallocation, no aliasing, better optimization |
Allocatable over pointer, Allocatable Arrays and Dynamic Memory |
Keep state in arguments or derived-type objects, not in module variables |
procedures stay testable, thread-safe and |
|
Report errors through |
libraries stay reusable |
An error reporting policy, Error Handling and Program Termination |
Compile with warnings and run-time checks during development |
the compiler is the cheapest reviewer |
Let the compiler review the code, Compilers and Compiler Options |
One program unit per file, file named after the unit |
build tools can find and order the sources |
|
Document the public interface with FORD comments |
the documentation lives next to the code |
Names, case and layout
The fortran-lang style guide asks for three things: write the keywords and constructs (do, subroutine, module) in
lower case; use lower case for other names, keep them short, ideally one or two syllables, and separate longer words with
underscores; and indent consistently, where the guide leaves the width to taste (two, three or four spaces are all
common). It prefers standard mathematical notation for mathematical symbols and warns against abbreviations that are
ambiguous: spline_interp is acceptable, splineint is not. The stdlib guide is stricter and is the better template for a
library: four spaces of indentation and no tabs, lines of 80 characters or fewer with 132 as a hard limit, intent on every
dummy argument, optional written after intent, at most one program, module or submodule per file, the file named after the unit
with the extension .f90 (or .F90 when it is preprocessed), and the unit name repeated on the end statement unless the
block is short.
Fortran itself is case-insensitive, so none of this is enforced by a compiler. The language does impose a few limits that a style should stay well inside of. A name has at most 63 characters (C601 in 6.2.2), and a free-form line has at most ten thousand characters in Fortran 2023 (6.3.2.1); the older limit of 132 characters is the figure the style guides still use as a portable upper bound for readability and for older compilers. The detailed rules of source form are in Program Structure and Source Form.
The recommended style in one place, in the module that holds the example for the rest of this page:
module heatflow_kinds_demo
use, intrinsic :: iso_fortran_env, only: int32, real64
implicit none (type, external)
private
public :: dp, ip
integer, parameter :: dp = real64
integer, parameter :: ip = int32
end module heatflow_kinds_demo
!> Residual history of an iterative solver.
module heatflow_history
use, intrinsic :: ieee_arithmetic, only: ieee_is_finite
use heatflow_kinds_demo, only: dp, ip
implicit none (type, external)
private
public :: history_t, relative_change
!> Growing record of residuals; components are private, use the procedures.
type :: history_t
private
real(dp), allocatable :: values(:)
integer(ip) :: count = 0_ip
contains
procedure :: push => history_push
procedure :: last => history_last
procedure :: size => history_size
end type history_t
contains
!> Relative change between two successive values; `pure elemental` so it works on scalars and arrays.
pure elemental function relative_change(new, old) result(change)
real(dp), intent(in) :: new, old
real(dp) :: change
change = abs(new - old)/max(abs(old), tiny(1.0_dp))
end function relative_change
!> Append a value. Reports failure through `stat` and `errmsg` instead of stopping.
subroutine history_push(self, value, stat, errmsg)
class(history_t), intent(inout) :: self
real(dp), intent(in) :: value
integer, intent(out), optional :: stat
character(len=*), intent(out), optional :: errmsg
if (present(stat)) stat = 0
if (present(errmsg)) errmsg = ''
if (.not. ieee_is_finite(value)) then
if (present(stat)) stat = 1
if (present(errmsg)) errmsg = 'history_push: value is not finite'
return
end if
if (.not. allocated(self%values)) allocate (self%values(8))
if (self%count == size(self%values)) self%values = [self%values, self%values]
self%count = self%count + 1_ip
self%values(self%count) = value
end subroutine history_push
pure function history_last(self) result(v)
class(history_t), intent(in) :: self
real(dp) :: v
v = merge(self%values(max(self%count, 1_ip)), 0.0_dp, self%count > 0_ip)
end function history_last
pure function history_size(self) result(n)
class(history_t), intent(in) :: self
integer(ip) :: n
n = self%count
end function history_size
end module heatflow_history
program use_history
use heatflow_kinds_demo, only: dp
use heatflow_history, only: history_t, relative_change
use, intrinsic :: ieee_arithmetic, only: ieee_value, ieee_quiet_nan
implicit none (type, external)
type(history_t) :: h
integer :: stat
character(len=64) :: msg
real(dp) :: bad
call h%push(1.0e-2_dp)
call h%push(4.0e-3_dp, stat, msg)
print '(a, i0, a, es9.2)', 'count=', h%size(), ' last=', h%last()
bad = ieee_value(bad, ieee_quiet_nan)
call h%push(bad, stat, msg) ! rejected: not finite
print '(a, i0, 2a)', 'stat=', stat, ' msg=', trim(msg)
print '(3es10.2)', relative_change([1.0e-2_dp, 5.0e-3_dp, 2.5e-3_dp], [2.0e-2_dp, 1.0e-2_dp, 5.0e-3_dp])
end program use_history
Output with gfortran 16.2 and with Flang 23 (identical):
count=2 last= 4.00E-03
stat=1 msg=history_push: value is not finite
5.00E-01 5.00E-01 5.00E-01
The module compiled without a warning under -std=f2023 -Wall -Wextra -pedantic -fcheck=all and with Flang 23.
Source: J3/23-007r1: 6.2.2 Names, 6.3.2 Free source form, 15.7 Pure procedures, 15.9 Elemental procedures, Fortran Programming Language: Best Practices, Fortran Style Guide and Fortran stdlib: Style guide
What the module shows, rule by rule:
-
All keywords are in lower case, names are lower case with underscores, the indentation is two spaces (the rule asks only for consistency), and every
endstatement repeats the name of its unit. -
Declarations use
::, and entity-style array declarations (real(dp), allocatable :: values(:)) rather than thedimensionattribute, as the stdlib guide asks. -
The type has
privatecomponents and a small set of type-bound procedures; the user ofhistory_tcannot depend on how the values are stored. See Derived Types and Type-Bound Procedures, Abstract Types and Finalization. -
The pure functions carry the
pure(andelemental) prefix, every dummy has anintent, and the one subroutine that can fail reports through optionalstatanderrmsgarguments. -
Documentation comments start with
!>, see Documenting the public interface.
Declare everything
Fortran 77 gave every undeclared name a type from its first letter, and that rule still exists unless it is switched
off. The statement implicit none (8.7) switches it off for a scoping unit; the Fortran 2018 form
implicit none (type, external) also requires every external procedure to be declared (the GNU Fortran status page for
Fortran 2018 lists the implicit-none-spec-list as supported). implicit none in a module applies to every procedure
the module contains and to its submodules, so one statement per module is enough, and every main program needs its own.
In this guide’s modules it is the first statement after the use statements.
A misspelled variable is the classic case. This program is legal Fortran with implicit typing, and prints a wrong answer without any message:
! Deliberately sloppy: no implicit none, so a misspelled name silently becomes a new variable.
program typo_demo
real :: residual
residual = 1.0
resdual = residual*0.5
print '(a, f4.2)', 'residual = ', residual
end program typo_demo
Built with plain gfortran 01_typo.f90 and run:
residual = 1.00
The assignment to resdual created a new real variable and the real result residual was never updated. With
implicit none, or with the flag -fimplicit-none that fpm and the verification script pass to GNU Fortran, the same program does not compile. These are the real
messages:
01_typo.f90:5:3:
5 | resdual = residual*0.5
| 1~~~~~~
Error: Symbol 'resdual' at (1) has no IMPLICIT type; did you mean 'residual'?
(gfortran 16.2), and for Flang 23, built with flang-23 -fimplicit-none:
error: Semantic errors in 01_typo.f90
01_typo.f90:5:3: error: No explicit type declared for 'resdual'
resdual = residual*0.5
^^^^^^^
The external half of the statement catches the call to a procedure that nobody declared. With implicit none (type, external) the
call below is an error instead of a link-time problem or, worse, a call with a mismatched argument list:
! Deliberately wrong: with implicit none (type, external) a call without an interface is an error.
program ext_demo
implicit none (type, external)
real :: x(3) = [1.0, 2.0, 3.0]
call halve(x, 3)
print *, x
end program ext_demo
gfortran 16.2 (-std=f2023) reports:
04_implicit_external.f90:5:18:
5 | call halve(x, 3)
| 1
Error: Procedure 'halve' called at (1) is not explicitly declared
and Flang 23:
error: Semantic errors in 04_implicit_external.f90
04_implicit_external.f90:5:8: error: 'halve' is an external procedure without the EXTERNAL attribute in a scope with IMPLICIT NONE(EXTERNAL)
call halve(x, 3)
^^^^^
Source: J3/23-007r1: 8.7 IMPLICIT statement and B.3.15 of the J3/26-007r1 draft, GNU Fortran manual: Fortran 2018 status and Options controlling Fortran dialect
The 2028 draft goes one step further: J3/26-007r1 lists default implicit typing among the obsolescent features
(B.3.15 of that draft; see What’s New and Fortran 2028 Preview). That
document is a committee draft, not a standard. Writing implicit none now makes a program immune to the change. For the
same reason fpm projects set implicit-typing = false and implicit-external = false in the [fortran] table of
fpm.toml, as heatflow does (Fortran Package Manager (fpm)); a plain fpm build of the
sources already passes -fimplicit-none to GNU Fortran.
Kinds and literals
Never write real*8, double precision, or rely on the default real having a particular precision. Define the kinds
once in a module, import only the names needed, and give every literal the kind of the variable it initializes. The
heatflow kinds module is the whole mechanism:
module heatflow_kinds
use, intrinsic :: iso_fortran_env, only: int32, int64, real32, real64
implicit none
private
public :: sp, dp, ip, lp
integer, parameter :: sp = real32
integer, parameter :: dp = real64
integer, parameter :: ip = int32
integer, parameter :: lp = int64
end module heatflow_kinds
Source: J3/23-007r1: 16.10.2.1 The ISO_FORTRAN_ENV intrinsic module and 16.9.183 SELECTED_REAL_KIND, Fortran Programming Language: Best Practices, Floating Point Numbers
The kind constants of iso_fortran_env (real32, real64, real128, int32, …) are a portable way to ask for a storage
size; selected_real_kind(p, r) asks for a precision and range instead. The two models, their pitfalls, and the fact that a
requested kind may not exist (real128 is a negative constant on a compiler that does not provide it) are in
Intrinsic Types and Kinds and
Intrinsic Modules and iso_fortran_env.
The literal is the part people forget. 0.1 is a default-real constant, which is converted to double precision after it
has been rounded to single precision:
program literal_kind
use, intrinsic :: iso_fortran_env, only: real64
implicit none (type, external)
integer, parameter :: dp = real64
real(dp) :: alpha_bad, alpha_good
alpha_bad = 0.1 ! default-real literal, converted to double afterwards
alpha_good = 0.1_dp ! literal has the kind of the variable
print '(a, es24.16)', 'bad : ', alpha_bad
print '(a, es24.16)', 'good: ', alpha_good
print '(a, es10.3)', 'difference: ', alpha_bad - alpha_good
end program literal_kind
gfortran 16.2 and Flang 23 print the same digits:
bad : 1.0000000149011612E-01
good: 1.0000000000000001E-01
difference: 1.490E-09
The compilers disagree on how loudly they complain. GNU Fortran says nothing under -Wall -Wextra; it needs
-Wconversion-extra:
15_literal_kind.f90:6:14:
6 | alpha_bad = 0.1 ! default-real literal, converted to double afterwards
| 1
Warning: Conversion from 'REAL(4)' to 'REAL(8)' at (1) [-Wconversion-extra]
Flang 23 warns by default, with a message that names the exact value that is lost:
15_literal_kind.f90:6:15: warning: Default real literal in REAL(8) context might need a kind suffix, as its rounded value 1.00000001490116119384765625e-1_4 is inexact [-Wreal-constant-widening]
alpha_bad = 0.1 ! default-real literal, converted to double afterwards
^^^
Source: J3/23-007r1: 7.4.3.2 Real type and 10.1.5.2 Numeric intrinsic operations, GNU Fortran manual: Options to request or suppress errors and warnings
Two further habits belong here. First, do not use -fdefault-real-8 (or the equivalent in other compilers) to fix a legacy code
base: it changes the kind of every default real, including the ones in calls to libraries that expect a default real, and
the standard’s rules about double precision stay true only by accident. Second, never compare reals with == unless
equality is exactly what is meant; -Wcompare-reals warns about it, and the floating-point page has the correct comparisons
(Floating-Point and IEEE Arithmetic).
Intent, pure and elemental
intent(in), intent(out) and intent(inout) (8.5.10) say how a dummy argument is used. They are not decoration: the
compiler rejects a write to an intent(in) dummy, and an intent(out) dummy is undefined on entry (and its allocatable
parts are deallocated, its defaulted components are initialized), which the optimizer can exploit. Missing an intent is
not an error, and no warning flag asks for it, so the rule has to be a house rule enforced in review or by a linter
(see Let the compiler review the code). The real diagnostics when the rule is broken:
! Deliberately wrong: assigning to an intent(in) dummy argument.
module intent_demo
implicit none
contains
subroutine scale(x, factor)
real, intent(inout) :: x(:)
real, intent(in) :: factor
factor = factor*2.0
x = x*factor
end subroutine scale
end module intent_demo
gfortran 16.2:
03_intent_error.f90:8:5:
8 | factor = factor*2.0
| 1~~~~~~
Error: Dummy argument 'factor' with INTENT(IN) in variable definition context (assignment) at (1)
Flang 23:
error: Semantic errors in 03_intent_error.f90
03_intent_error.f90:8:5: error: Left-hand side of assignment is not definable
factor = factor*2.0
^^^^^^
03_intent_error.f90:8:5: because: 'factor' is an INTENT(IN) dummy argument
factor = factor*2.0
^^^^^^
03_intent_error.f90:7:25: Declaration of 'factor'
real, intent(in) :: factor
^^^^^^
An intent(out) argument that is never set is detected by -Wall in GNU Fortran, together with an unused local variable:
! Deliberately sloppy but legal: unused dummy argument, unused variable, narrowing-prone literal.
module sloppy_demo
use, intrinsic :: iso_fortran_env, only: real64
implicit none
integer, parameter :: dp = real64
contains
subroutine relax(u, omega, scratch)
real(dp), intent(inout) :: u(:)
real(dp), intent(in) :: omega
real(dp), intent(out) :: scratch(:)
integer :: i, unused_counter
do i = 2, size(u) - 1
u(i) = u(i) + 0.25*omega*(u(i - 1) - 2.0_dp*u(i) + u(i + 1))
end do
end subroutine relax
end module sloppy_demo
program run_sloppy
use sloppy_demo
implicit none
real(dp) :: u(5) = [0.0_dp, 0.0_dp, 4.0_dp, 0.0_dp, 0.0_dp], work(5)
call relax(u, 1.0_dp, work)
print '(5f6.2)', u
end program run_sloppy
Compiling with gfortran -std=f2023 -Wall -Wextra -pedantic:
02_unused_and_conversion.f90:7:30:
7 | subroutine relax(u, omega, scratch)
| 1~~~~~~
Warning: Dummy argument 'scratch' at (1) was declared INTENT(OUT) but was not set [-Wunused-dummy-argument]
02_unused_and_conversion.f90:11:19:
11 | integer :: i, unused_counter
| 1~~~~~~~~~~~~~
Warning: Unused variable 'unused_counter' declared at (1) [-Wunused-variable]
and the program, which is legal, prints the following (the single-precision constant 0.25 is silently widened; it is exactly
representable, so the answer is right, but -Wconversion-extra flags it as in the previous section):
0.00 1.00 2.25 0.56 0.00
Source: J3/23-007r1: 8.5.10 INTENT attribute and 15.5.2.4 Argument association, GNU Fortran manual: Options to request or suppress errors and warnings
A procedure that has no side effects should say so with pure (15.7), and one that acts on every element of an array
argument identically should be elemental (15.9; an elemental procedure is pure unless it says impure). The
payoff is concrete: a pure procedure may be called from a do concurrent construct and from another pure procedure,
an elemental one works on a scalar and on arrays of any rank with one definition (relative_change above is called with
rank-1 arrays), and the compiler enforces the contract. The restrictions are checked: a pure procedure may not assign to
a module variable.
! Deliberately wrong: a pure function may not modify module state.
module pure_state
implicit none (type, external)
integer :: calls = 0
contains
pure function scaled(x) result(y)
real, intent(in) :: x
real :: y
calls = calls + 1
y = 2.0*x
end function scaled
end module pure_state
gfortran 16.2:
12_pure_global_error.f90:9:5:
9 | calls = calls + 1
| 1~~~~~~
Error: Variable 'calls' cannot appear in a variable definition context (assignment) at (1) in PURE procedure
Flang 23:
error: Semantic errors in 12_pure_global_error.f90
12_pure_global_error.f90:9:5: error: Left-hand side of assignment is not definable
calls = calls + 1
^^^^^
12_pure_global_error.f90:9:5: because: 'calls' may not be defined in pure subprogram 'scaled' because it is host-associated
calls = calls + 1
^^^^^
12_pure_global_error.f90:4:14: Declaration of 'calls'
integer :: calls = 0
^^^^^
The pure kernels of heatflow (jacobi_sweep, ftcs_sweep) are the same discipline applied to a numerical core. Do not
mark everything pure for its own sake: a routine that must print, read a file or measure time cannot be, and the Fortran 2018
permission for error stop in a pure procedure (Error Handling and Program Termination)
is meant for assertion failures, not for control flow. Use value (8.5.19) only for scalars passed by value, mainly for C
interoperability (Interoperability with C).
Modules, private by default and use only
Every procedure belongs in a module (or is an internal procedure): a module procedure has an explicit interface by
construction, so the compiler checks every call, and no interface block ever has to be written by hand and kept in sync.
Three habits make the module a good unit of design:
-
A bare
privatestatement first. Without it every helper is public (the default accessibility ispublic). The bareprivatemakes the module a closed unit and thepublic ::list becomes its API. Accessibility is in 8.5.2 and 8.6.1; the full treatment, includingprotected, is in Modules and Submodules. -
use ..., only:everywhere. Theonlylist says which names the unit needs and where they come from. Without it the unit depends on everything the module exports, today and after the next release of the module. -
Private type components. The type name is public and the data is not; users reach the data through procedures and the library keeps the freedom to change the representation.
Here is what the compiler says about the missing only clause (GNU Fortran, -Wuse-without-only, not part of -Wall),
for the program in 02_unused_and_conversion.f90:
02_unused_and_conversion.f90:19:7:
19 | use sloppy_demo
| 1
Warning: USE statement at (1) has no ONLY qualifier [-Wuse-without-only]
The cost of omitting it is paid later, when two modules export the same name; that failure, with the real diagnostic, is
in Modules and Submodules. A subtle point: a bare private in a module also applies to names the module merely uses,
so a module that needs to re-export a kind must say so with a public statement; the heatflow kinds module does exactly that
with public :: sp, dp, ip, lp.
Source: J3/23-007r1: 8.5.2 Accessibility attribute, 8.6.1 Accessibility statement and 14.2.2 The USE statement and use association, Fortran Programming Language: Best Practices, Modules and programs
When an interface must stay stable while the implementation changes, the module declares the interface and a submodule
supplies the body. heatflow does this for its solvers: heatflow_solver holds the abstract solver_t and the
interfaces, and the submodules heatflow_jacobi and heatflow_ftcs implement the steps, so changing a numerical scheme does not
recompile the code that uses the module.
Allocatable over pointer
Prefer allocatable for dynamic memory. An allocatable object is deallocated automatically when it goes out of
scope, it cannot be aliased, it can be copied by assignment (with automatic reallocation) and a function can return it. A
pointer can point at anything with the target attribute and at arbitrary heap memory, which is sometimes exactly what is
needed (linked structures, views of a subobject; see Pointers and Targets), but it is never
freed unless someone remembers to.
The next program calls a function that returns an allocatable array a thousand times and one that returns a pointer a thousand times. The first needs no cleanup code; the second leaks every call.
module profile_demo
use, intrinsic :: iso_fortran_env, only: real64
implicit none (type, external)
private
public :: dp, row_profile, row_profile_ptr
integer, parameter :: dp = real64
contains
!> Preferred: an allocatable result is freed automatically after use.
function row_profile(u, j) result(p)
real(dp), intent(in) :: u(:, :)
integer, intent(in) :: j
real(dp), allocatable :: p(:)
p = u(:, j)*0.5_dp
end function row_profile
!> Avoid: a pointer result must be deallocated by the caller, or it leaks.
function row_profile_ptr(u, j) result(p)
real(dp), intent(in) :: u(:, :)
integer, intent(in) :: j
real(dp), pointer :: p(:)
allocate (p(size(u, 1)))
p = u(:, j)*0.5_dp
end function row_profile_ptr
end module profile_demo
program leak_check
use profile_demo, only: dp, row_profile, row_profile_ptr
implicit none (type, external)
real(dp) :: u(100, 100)
real(dp), pointer :: q(:)
integer :: k
real(dp) :: total
u = 1.0_dp
total = 0.0_dp
do k = 1, 1000
total = total + sum(row_profile(u, 3)) ! no cleanup code needed
end do
do k = 1, 1000
q => row_profile_ptr(u, 3)
total = total + sum(q)
end do ! q is never deallocated: 1000 leaks
print '(a, f10.1)', 'total = ', total
end program leak_check
Both compilers print the same total:
total = 100000.0
Running the gfortran build (gfortran -g -O0) under Valgrind 3.x in the verification container, filtered to the summary
lines:
==12== in use at exit: 800,000 bytes in 1,000 blocks
==12== total heap usage: 2,019 allocs, 1,019 frees, 1,610,263 bytes allocated
==12== 800,000 bytes in 1,000 blocks are definitely lost in loss record 1 of 1
==12== definitely lost: 800,000 bytes in 1,000 blocks
==12== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)
The 1,000 lost blocks are exactly the pointer results; the allocatable results were all freed. (The block and byte counts are those of this run and depend on the runtime.)
Source: J3/23-007r1: 8.5.3 ALLOCATABLE attribute, 8.5.14 POINTER attribute and 9.7.3.2 Deallocation of allocatable variables, Fortran Programming Language: Best Practices, Allocatable Arrays, Valgrind Memcheck manual
Related advice: use assumed-shape (real(dp), intent(in) :: u(:, :)) dummies rather than explicit-size or assumed-size
ones, and let size(u, 1) give the extent, as the heatflow kernels do
(Array Arguments: Assumed Shape, Size and Rank); use whole-array
operations and intrinsics instead of loops where they are clearer (Array Intrinsics and Reductions);
and when the order of loops matters for speed, write the first index innermost
(Performance and Memory Layout).
Avoiding global state
A module variable has the save attribute implicitly (8.5.16): it lives for the whole run and every user of the module
sees the same one. That is occasionally right (a table that is read-only after initialization, a named constant) and
usually a trap: the procedures that touch it are no longer pure, cannot be called concurrently, and give different
answers on identical arguments. The alternative is to put the state in a derived type and pass the object:
module step_counter_global
implicit none (type, external)
private
public :: tick, relax_global
integer :: calls = 0 ! hidden global state: every caller shares it
contains
integer function tick()
calls = calls + 1
tick = calls
end function tick
subroutine relax_global(u)
real, intent(inout) :: u(:)
integer :: n
n = tick() ! side effect: the routine is not pure
u = u*0.5
end subroutine relax_global
end module step_counter_global
module step_counter_explicit
implicit none (type, external)
private
public :: counter_t, relax_explicit
type :: counter_t
integer :: calls = 0
end type counter_t
contains
subroutine relax_explicit(u, counter)
real, intent(inout) :: u(:)
type(counter_t), intent(inout) :: counter
counter%calls = counter%calls + 1 ! state is an argument: visible, testable, per-instance
u = u*0.5
end subroutine relax_explicit
end module step_counter_explicit
program compare_state
use step_counter_global, only: tick, relax_global
use step_counter_explicit, only: counter_t, relax_explicit
implicit none (type, external)
real :: u(3) = 1.0
type(counter_t) :: a, b
call relax_global(u)
call relax_global(u)
print '(a, i0)', 'global: next tick is ', tick()
call relax_explicit(u, a)
call relax_explicit(u, b)
call relax_explicit(u, b)
print '(a, i0, a, i0)', 'explicit: a=', a%calls, ' b=', b%calls
end program compare_state
Output of both compilers:
global: next tick is 3
explicit: a=1 b=2
The global counter has no owner: relax_global bumped it twice behind the caller’s back and the third call to tick returns 3. The
explicit counters are per object and each routine’s effect is visible in its argument list. The same reasoning applies to
common blocks, whose use is legacy
(Legacy Features and Modernization), and to variables with save: if a state must be shared,
make it a component of a type that the caller creates, and if the state is protected configuration, protected (8.5.15) lets
users read it without being able to change it.
Also keep module variables out of default initialization traps: an initialization in a declaration of a local variable
(integer :: n = 0) implies save and runs only once, which is the single most common surprise in this area
(Common Gotchas and Pitfalls).
An error reporting policy
A reusable procedure should not decide that the program must die. The policy that composes well:
-
Library routines validate their input and report problems through optional or mandatory
statanderrmsgarguments, or through a status component of the result, ashistory_pushabove and the standard intrinsics (allocate ... stat=,open ... iostat=) do. -
Only the application layer (the main program or a command-line driver) turns a failure into
error stopwith a non-zero code and a message. -
Programming errors, as opposed to run-time conditions, can be checked with assertions or
error stopclose to the fault, in a build mode that is described in the project documentation.
module heatflow_params_demo
use, intrinsic :: iso_fortran_env, only: real64
implicit none (type, external)
private
public :: dp, stable_dt
integer, parameter :: dp = real64
contains
!> Largest stable FTCS time step; the library reports problems and never stops the program.
subroutine stable_dt(alpha, dx, dt, stat, errmsg)
real(dp), intent(in) :: alpha, dx
real(dp), intent(out) :: dt
integer, intent(out) :: stat
character(len=*), intent(out) :: errmsg
stat = 0
errmsg = ''
dt = 0.0_dp
if (alpha <= 0.0_dp .or. dx <= 0.0_dp) then
stat = 1
errmsg = 'stable_dt: alpha and dx must be positive'
return
end if
dt = dx**2/(4.0_dp*alpha)
end subroutine stable_dt
end module heatflow_params_demo
program choose_policy
use heatflow_params_demo, only: dp, stable_dt
implicit none (type, external)
real(dp) :: dt
integer :: stat
character(len=80) :: msg
call stable_dt(1.0e-2_dp, 0.1_dp, dt, stat, msg)
print '(a, es10.3, a, i0)', 'dt = ', dt, ' stat = ', stat
call stable_dt(-1.0_dp, 0.1_dp, dt, stat, msg)
if (stat /= 0) then
print '(2a)', 'fatal: ', trim(msg) ! the application decides what a failure costs
error stop 2
end if
print *, 'not reached'
end program choose_policy
Run with gfortran 16.2; the program’s own output is on standard output, the diagnostic on standard error, and the exit status is 2:
dt = 2.500E-01 stat = 0
fatal: stable_dt: alpha and dx must be positive
ERROR STOP 2
Error termination. Backtrace:
#0 ... in ???
#3 ... in choose_policy
at /work/13_error_policy.f90:37
#4 ... in main
at /work/13_error_policy.f90:27
(The addresses of the backtrace are omitted here, they change from run to run.) Flang 23 prints Fortran ERROR STOP: code 2
followed by IEEE arithmetic exceptions signaled: INEXACT and also exits with status 2; the second line is the runtime’s
report of the floating-point flags that were raised and has nothing to do with the program’s fault. The details of
stop, error stop, iostat, iomsg and the exit codes are in
Error Handling and Program Termination, and the same policy for file
operations in Input/Output: Units and Files.
Let the compiler review the code
The strictest tool is the compiler that will build the program in production, run with its checking options during development.
The set used to check every example on these pages is -std=f2023 -Wall -Wextra -pedantic -fimplicit-none -fcheck=all -g
-fbacktrace. -std=f2023 makes non-standard syntax an error; -fcheck=all adds run-time bounds, pointer, recursion and
allocation checks; -Wall -Wextra are the warnings that rarely produce noise. A few warnings are off by default and worth
enabling for library code (all of these flags exist in GNU Fortran 16.2):
| Flag | What it catches |
|---|---|
|
implicit conversions between kinds and types, including the |
|
a call to a procedure that has no explicit interface |
|
a call to a procedure that was never declared |
|
a |
|
equality comparison of reals (already part of |
|
assignments that reallocate the left-hand side, a hidden allocation in a hot loop |
|
array temporaries the compiler had to create for an argument or an expression |
|
a user procedure with the name of an intrinsic |
|
arguments and named constants never used |
Flang has its own switches; its default warnings already include the literal-widening message shown above, and flang --help
is the reference. Recommended option sets for developer, CI, release and cluster builds, with the evidence behind each flag, are in
Compilers and Compiler Options. A caveat that surprises many projects: a plain
fpm build of fpm 0.13.0 passed no warning or run-time-check flags apart from -fimplicit-none and
-Werror=implicit-interface; the profile has to be named (fpm build --profile debug) to get them
(Fortran Package Manager (fpm)).
Formatting is better done by a tool than by review. fprettify (community, Python) and findent re-indent and normalise
spacing; linters such as Fortitude check rules that a compiler does not. They are community
tools with their own options and versions, and their use, configuration and the diffs they produce are in
Documentation and Editor Tooling. Whatever is chosen, run it in CI, so that the style rules are not a matter
of opinion in review. Tests belong to the same discipline: a failing test is a better reviewer than a warning
(Testing, Debugging and Profiling).
Project layout and API design for libraries
An fpm project is the simplest layout that satisfies every rule above: library sources under src/, the command-line program under app/,
tests under test/, examples under example/, and a manifest fpm.toml. The heatflow project in the verification
directory looks like this (build directories and generated files omitted; parallel/, python/ and mex/ hold
the C, MPI, coarray, OpenMP, f2py and MATLAB variants that other pages build by hand):
heatflow/
fpm.toml
CMakeLists.txt
heatflow.nml
include/heatflow.h
src/
heatflow_kinds.f90 heatflow_constants.f90 heatflow_boundary.f90
heatflow_grid.f90 heatflow_kernels.f90 heatflow_solver.f90
heatflow_jacobi.f90 heatflow_ftcs.f90 (submodules of heatflow_solver)
heatflow_config.f90 heatflow_io.f90 heatflow_capi.f90
app/main.f90
example/do_concurrent_stencil.f90
test/check.f90 test/test_grid.f90
parallel/ python/ mex/
Design rules that this layout expresses:
-
One program unit per file, file named after the unit.
heatflow_grid.f90contains moduleheatflow_grid. Build tools (fpm, CMake, Meson) find the dependency order from theusestatements, and a person finds the file from the name. The build order and module files are described in Building with Make, CMake and Meson. -
A common prefix on every public module. Module names are global to the program (14.2.1), so two libraries with a module
gridcollide at link time or overwrite each other’s.modfile. The prefixheatflow_is the convention, and fpm’s registry has a module-naming rule for published packages. -
Layers, with no cycles.
heatflow_kindsandheatflow_constantsdepend on nothing,heatflow_boundary(which needs onlyiso_c_binding) andheatflow_kernels(kinds only) sit next to them,heatflow_griduses the boundary module,heatflow_iothe grid,heatflow_configthe boundary and constants,heatflow_solverthe grid and the boundary module, and its submodules the kernels. A cyclicuseis illegal; the cure is a submodule or a third module that both depend on (Modules and Submodules). -
The numerical kernels are
pureprocedures on plain arrays; the types and I/O sit around them. That is why the same kernel serves the Fortran solver, the C interfaceheatflow_capi, the OpenMP, MPI and coarray variants and the Python wrapper. -
A small, stable public surface. Procedures a client may call are
public; everything else, including helper functions and the module’s own state, isprivate. Optional arguments and keyword arguments let a routine grow without breaking its callers (Procedures, Functions and Subroutines). Extension points are abstract types with deferred bindings (solver_t), not flags (Object Oriented Programming). -
Configuration in a namelist, results through streams or CSV.
heatflow_configreads a/heatflow/namelist withstatandmsgarguments instead of aborting (Internal Files, Namelist and Non-Advancing I/O). -
A C interface that is a separate module.
heatflow_capiis the only module withbind(c); the rest of the library stays idiomatic (Interoperability with C). -
Enumerations. The boundary kinds are an
enum, bind(c)inheatflow_boundarybecause neither GNU Fortran 16.2 nor Flang 23 (by default) accepts the Fortran 2023enumeration type; see Enumerations. -
Tests and docs ship with the library.
fpm testrunstest/check.f90through the test-drive framework, the doc comments are in the sources, andexample/programs double as usage documentation (Fortran stdlib follows the same layout and is a good model for a larger library).
The fpm manifest that goes with it, shortened to what matters for style:
name = "heatflow"
version = "0.1.0"
license = "MIT"
[build]
auto-executables = true
auto-tests = true
auto-examples = true
[install]
library = true
[fortran]
implicit-typing = false
implicit-external = false
source-form = "free"
[dev-dependencies]
test-drive = { git = "https://github.com/fortran-lang/test-drive", tag = "v0.6.1" }
Source: fpm manifest reference and Fortran stdlib: Style guide
If the library will also be built with CMake or Meson, keep the same layout and let each build system glob or list the
src/ files; heatflow/CMakeLists.txt in the verification project does so. Versioning, installation and the registry are
in Fortran Package Manager (fpm).
Documenting the public interface
Document every public and protected entity and each of its arguments, as the stdlib guide asks. The tool is FORD, which reads
comments that start with !> (before an entity) or !! (after it, or at the end of a declaration line), takes
Markdown, recognises @note, @warning and @todo, and generates a site with call graphs. A short comment that says what
the procedure does and which conditions it requires is worth more than a long one that repeats the declaration. The
!> comments in the modules of this page are of this kind. FORD’s installation, project file, graphs, the exact
comment forms (checked against FORD 7.0.13) and the fortls language server are in
Documentation and Editor Tooling; nothing in the standard defines documentation comments.
The book authors' recommendations
One of the two reference books on modern Fortran closes with an appendix that lists constructs its authors recommend against, under the heading of deprecated features. That list is useful advice and is repeated here, but it is not the standard’s: the standard maintains its own lists of deleted and obsolescent features in Annex B, and most of the constructs below are fully current. The table separates the two. The "Annex B status" column was checked against Annex B of J3/23-007r1 (B.1 deleted from Fortran 90, B.2 deleted from Fortran 2008, B.3 obsolescent).
| Construct | Annex B status in Fortran 2023 | Style advice here |
|---|---|---|
|
not listed; the |
fine for a generated or shared fragment; a module is better for code (Generic and Rank Independent Programming uses include files only as a genericity technique and says why a module is better) |
|
not listed |
use |
|
not listed |
either form is correct; a plain |
|
plain |
use constructs, |
|
not listed |
write |
Implicit interfaces (an |
not listed |
use module procedures; enforce with |
|
|
avoid unless a layout is required; use |
Non-default |
not listed in Fortran 2023; default implicit typing is listed as obsolescent in the J3/26-007r1 draft (B.3.15) |
always |
|
not listed |
keep it for variables changed outside the program’s view, such as by C code or an asynchronous signal handler, and prefer |
|
not listed |
a low-level fence; prefer the higher-level image control statements (Coarrays, Images and Synchronization) |
The obsolescent list of the standard itself is: alternate return, computed go to, statement functions, data statements among executable
statements, assumed-length character functions, fixed source form, character* declarations, entry, labelled do,
common, equivalence and block data, the specific names of intrinsic functions, and forall
(B.3.2 to B.3.13). Arithmetic if and the non-block do construct are listed in B.2 as deleted Fortran 2008 features; B.1 lists the real
and double-precision do variable, pause, assign and assigned go to, the H edit descriptor and vertical format control
as deleted Fortran 90 features. Their treatment, with the old code shown and labelled as legacy and the modern
replacement, is only in Legacy Features and Modernization.
Where the books are outdated
| Topic | What changed |
|---|---|
The deprecation appendix |
the authors' list is advice, not the standard’s list; the obsolescent and deleted features are in Annex B, and the Fortran 2028 draft adds default implicit typing and one edit descriptor form to the obsolescent list. Verify each construct against Annex B before calling it obsolete. |
Style guidance |
the book material is about the language, not about style; community guides (fortran-lang, stdlib) and tools (fprettify, FORD, fortls, fpm) now exist and are what a new project adopts. |
Line length |
the 132-character limit quoted by older material is the Fortran 2018 limit; Fortran 2023 allows ten thousand characters per line, but the style guides keep 80 as the target and 132 as the hard limit. |
Build and project layout |
textbooks show a single source file or a |
Compiler flags |
old material names |
Testing |
no unit-test frameworks; test-drive, pFUnit and others are now routine. |
Standard version provenance and compiler support
| Practice or feature | Introduced | Support checked on 2026-10-19 |
|---|---|---|
|
Fortran 90 or later (see the history page for the edition of each) |
all compilers accept the examples |
|
Fortran 95 or later (see the history page) |
GNU Fortran 16.2 and Flang 23 accept |
|
Fortran 2003 |
both |
|
Fortran 2018 (GNU Fortran Fortran 2018 status page) |
both accept; both reject the undeclared external as shown above |
|
Fortran 2018 |
GNU Fortran status page lists it as supported |
Free-form line of up to ten thousand characters |
Fortran 2023 (6.3.2.1) |
not tested here; a long line should still wrap near 80 characters |
Default implicit typing obsolescent |
draft Fortran 2028 only (committee draft stage) |
no compiler diagnoses it by default |
Not checked here, because no compiler for them is installed: Intel ifx, NAG, HPE Cray and NVIDIA HPC compilers; see History, Standards and Compilers for the per-compiler support table.
Pitfalls
-
Treating a style guide as the standard. Both community guides and the book appendix are advice; a construct they discourage may still be perfectly valid and is sometimes the right tool.
-
implicit nonein the module but not in the main program. The main program is its own scoping unit; each needs the statement. -
A bare
privatethat also hides a re-exported kind or type. Add apublicstatement for each name that users of the module are meant to see. -
intent(out)used to keep a value. The dummy is undefined on entry; useintent(inout). -
Declaring
pureand then adding a print for debugging. The compiler will refuse; keep a separate impure wrapper or useimpure elemental. -
Using
0.1for a double-precision constant. It is rounded to single precision first, and the error is about1.5e-9, as the program above printed. -
Assuming a plain
fpm buildwarns. In fpm 0.13.0 it passes no warning or run-time-check flags apart from-fimplicit-noneand-Werror=implicit-interface. -
A module variable instead of an argument. The procedure loses
pure, thread safety and testability. -
Reaching for
pointerout of habit from C. The leak in the example above is the typical cost; useallocatable. -
Stopping the program inside a library.
stopanderror stoptake the decision away from the caller; return a status. -
Giving two libraries the same module name. The names are global; use a prefix.
-
Reformatting and refactoring in one commit. Run the formatter alone, test, and commit before the change that matters.
References
-
J3/26-007r1, the Fortran 2028 working draft (committee draft stage): B.3.15 Default implicit typing
-
Fortran Programming Language: Best Practices and Fortran Style Guide
-
Fortran Programming Language: Best Practices, Modules and programs, Allocatable arrays and Floating point numbers
-
GNU Fortran manual: Options to request or suppress errors and warnings, Options controlling Fortran dialect and Fortran 2018 status