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 implicit none (type, external) in every module and program

a misspelled name becomes a compile-time error instead of a silent new variable; a call to an undeclared external procedure is rejected

Declare everything, Variables, Declarations and Attributes

Define kinds once in a module, write literals with the kind suffix

portable precision; no default-real surprises

Kinds and literals, Intrinsic Types and Kinds

Give every dummy argument an intent

documents the data flow and lets the compiler reject writes to inputs

Intent, pure and elemental, Procedures, Functions and Subroutines

Mark procedures pure or elemental when they can be

no side effects, usable in do concurrent and in array expressions

Intent, pure and elemental

Put every procedure in a module

explicit interfaces for free, no hand-written interface blocks

Modules and Submodules

Make modules private by default and list the public interface

the interface is small and deliberate; internals can change

Modules, private by default and use only

Use use module, only: ...

every name is traceable to its module; no accidental clashes

Modules, private by default and use only

Prefer allocatable to pointer

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 pure

Avoiding global state

Report errors through stat/errmsg or a result; stop only in the application

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

Project layout and API design for libraries

Document the public interface with FORD comments

the documentation lives next to the code

Documenting the public interface

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.

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 end statement repeats the name of its unit.

  • Declarations use ::, and entity-style array declarations (real(dp), allocatable :: values(:)) rather than the dimension attribute, as the stdlib guide asks.

  • The type has private components and a small set of type-bound procedures; the user of history_t cannot depend on how the values are stored. See Derived Types and Type-Bound Procedures, Abstract Types and Finalization.

  • The pure functions carry the pure (and elemental) prefix, every dummy has an intent, and the one subroutine that can fail reports through optional stat and errmsg arguments.

  • 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)
         ^^^^^

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

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
                ^^^

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

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 private statement first. Without it every helper is public (the default accessibility is public). The bare private makes the module a closed unit and the public :: list becomes its API. Accessibility is in 8.5.2 and 8.6.1; the full treatment, including protected, is in Modules and Submodules.

  • use ..., only: everywhere. The only list 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.

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

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:

  1. Library routines validate their input and report problems through optional or mandatory stat and errmsg arguments, or through a status component of the result, as history_push above and the standard intrinsics (allocate ... stat=, open ... iostat=) do.

  2. Only the application layer (the main program or a command-line driver) turns a failure into error stop with a non-zero code and a message.

  3. Programming errors, as opposed to run-time conditions, can be checked with assertions or error stop close 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

-Wconversion-extra

implicit conversions between kinds and types, including the 0.1 literal above

-Wimplicit-interface

a call to a procedure that has no explicit interface

-Wimplicit-procedure

a call to a procedure that was never declared

-Wuse-without-only

a use statement with no only list

-Wcompare-reals

equality comparison of reals (already part of -Wextra)

-Wrealloc-lhs, -Wrealloc-lhs-all

assignments that reallocate the left-hand side, a hidden allocation in a hot loop

-Warray-temporaries

array temporaries the compiler had to create for an argument or an expression

-Wintrinsic-shadow

a user procedure with the name of an intrinsic

-Wunused-dummy-argument, -Wunused-parameter

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.f90 contains module heatflow_grid. Build tools (fpm, CMake, Meson) find the dependency order from the use statements, 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 grid collide at link time or overwrite each other’s .mod file. The prefix heatflow_ is the convention, and fpm’s registry has a module-naming rule for published packages.

  • Layers, with no cycles. heatflow_kinds and heatflow_constants depend on nothing, heatflow_boundary (which needs only iso_c_binding) and heatflow_kernels (kinds only) sit next to them, heatflow_grid uses the boundary module, heatflow_io the grid, heatflow_config the boundary and constants, heatflow_solver the grid and the boundary module, and its submodules the kernels. A cyclic use is illegal; the cure is a submodule or a third module that both depend on (Modules and Submodules).

  • The numerical kernels are pure procedures on plain arrays; the types and I/O sit around them. That is why the same kernel serves the Fortran solver, the C interface heatflow_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, is private. 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_config reads a /heatflow/ namelist with stat and msg arguments instead of aborting (Internal Files, Namelist and Non-Advancing I/O).

  • A C interface that is a separate module. heatflow_capi is the only module with bind(c); the rest of the library stays idiomatic (Interoperability with C).

  • Enumerations. The boundary kinds are an enum, bind(c) in heatflow_boundary because neither GNU Fortran 16.2 nor Flang 23 (by default) accepts the Fortran 2023 enumeration type; see Enumerations.

  • Tests and docs ship with the library. fpm test runs test/check.f90 through the test-drive framework, the doc comments are in the sources, and example/ 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" }

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

include line

not listed; the include line is standard (6.4 Including source text)

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)

double precision

not listed

use real(dp) with the kinds module, so precision is a property of the program and not of the declaration

do while (cond)

not listed

either form is correct; a plain do with exit handles the cases do while cannot (Control Flow)

go to

plain go to is not listed; the computed go to is obsolescent (B.3.3), the assigned go to is deleted (B.1)

use constructs, exit, cycle and select case

.eq., .lt. and friends

not listed

write ==, <, >=; shorter and the same in other languages (Expressions and Assignment)

Implicit interfaces (an external procedure called without an interface)

not listed

use module procedures; enforce with implicit none (external) as in Declare everything

sequence types and storage association

sequence itself is not listed; equivalence and common are obsolescent (B.3.11)

avoid unless a layout is required; use bind(c) for a layout contract (Derived Types)

Non-default implicit statements

not listed in Fortran 2023; default implicit typing is listed as obsolescent in the J3/26-007r1 draft (B.3.15)

always implicit none

volatile

not listed

keep it for variables changed outside the program’s view, such as by C code or an asynchronous signal handler, and prefer atomic operations and synchronization for concurrent data (Coarray Teams, Events, Collectives and Atomics)

sync memory

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 Makefile; fpm, CMake and Meson, one unit per file, and a tested library layout are now normal.

Compiler flags

old material names -fbounds-check, -ffloat-store and so on; -fcheck=all, -Wall -Wextra, and the Flang equivalents are the ones to use (Compilers and Compiler Options).

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

implicit none, intent, pure, modules, private/public, allocatable dummies and results

Fortran 90 or later (see the history page for the edition of each)

all compilers accept the examples

elemental procedures

Fortran 95 or later (see the history page)

GNU Fortran 16.2 and Flang 23 accept

protected, use, intrinsic

Fortran 2003

both

implicit none (type, external)

Fortran 2018 (GNU Fortran Fortran 2018 status page)

both accept; both reject the undeclared external as shown above

error stop allowed in a pure procedure

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 none in the module but not in the main program. The main program is its own scoping unit; each needs the statement.

  • A bare private that also hides a re-exported kind or type. Add a public statement 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; use intent(inout).

  • Declaring pure and then adding a print for debugging. The compiler will refuse; keep a separate impure wrapper or use impure elemental.

  • Using 0.1 for a double-precision constant. It is rounded to single precision first, and the error is about 1.5e-9, as the program above printed.

  • Assuming a plain fpm build warns. In fpm 0.13.0 it passes no warning or run-time-check flags apart from -fimplicit-none and -Werror=implicit-interface.

  • A module variable instead of an argument. The procedure loses pure, thread safety and testability.

  • Reaching for pointer out of habit from C. The leak in the example above is the typical cost; use allocatable.

  • Stopping the program inside a library. stop and error stop take 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.