Modules and Submodules

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-18. Every example was compiled warning-free with gfortran -std=f2023 -Wall -Wextra -pedantic -fimplicit-none -fcheck=all (GNU Fortran 16.2) and run, and the submodule examples were also built and run with LLVM Flang 23; the output shown is real output. Examples that are deliberately wrong, or that use a feature the installed compilers reject, are labelled as such and show the real diagnostics. Modules and submodules are defined in clause 14 of the standard ("Program units"): 14.2.1 gives the module syntax, 14.2.2 the use statement and use association, and 14.2.3 submodules. Accessibility is in 8.5.2 and 8.6.1, protected in 8.5.15, save in 8.5.16 and separate module procedures in 15.6.2.5.

A module is the unit in which modern Fortran packages data, named constants, types, interfaces and procedures, and the only portable way to give a procedure an explicit interface without writing it twice. Everything in the running example, heatflow, lives in modules: heatflow_kinds, heatflow_constants, heatflow_boundary, heatflow_grid, heatflow_kernels, heatflow_solver, heatflow_config, heatflow_io and heatflow_capi. A submodule extends a module with the bodies of procedures whose interfaces the module declares, so that implementation details can change without touching the module file that every other unit compiles against. In heatflow_solver the interfaces live in the module and the Jacobi and FTCS bodies in the submodules heatflow_jacobi and heatflow_ftcs. This page covers the mechanics, the pitfalls, and the file and build-order consequences; the procedures themselves are in Procedures, Functions and Subroutines, generic interfaces and defined operators in Interfaces, Generics and Operator Overloading, the intrinsic modules in Intrinsic Modules and iso_fortran_env, and the build tools that order the compilation in Building with Make, CMake and Meson and Fortran Package Manager (fpm).

What a module is

A module (14.2.1) has a specification part and an optional module-subprogram part introduced by contains:

module name
  ! specification part: use, implicit none, accessibility, declarations, interfaces
contains
  ! module procedures
end module name

The module name is global to the program (14.2.1, Note 1, and 19.2), so a program cannot contain two modules with the same name. The end module statement may repeat the name (end module heatflow_grid), which the compiler checks (C1402).

Entity Notes Where it goes

Named constants and variables

module variables have the save attribute implicitly (8.5.16)

specification part

Derived types, enumerations

type definitions and enum, bind(c) blocks, see Derived Types

specification part

Interface blocks, abstract interfaces, generics

explicit interfaces for external code, generic names, defined operators

specification part

Namelist groups

namelist /heatflow/ nx, ny, ... can be public

specification part

Procedures

function and subroutine with explicit interfaces by construction

after contains

Separate module procedure bodies

module procedure name, or module function/module subroutine

after contains (usually in a submodule)

A module specification part cannot contain a statement function, an entry statement or a format statement (C1403), although all three may appear inside a module subprogram. implicit none written in the module specification part applies to every module procedure, which is why implicit none (type, external) is the first statement after the use statements in the heatflow modules. An empty contains is allowed (R1407), which is useful when a build tool or a template inserts procedures later. A module may use other modules, but it may not reference itself directly or indirectly (14.2.2); see Breaking cyclic dependencies with submodules.

A module can also be provided by something other than Fortran, and a module supplied by the processor is an intrinsic module (14.2.1). Procedures and types defined in an intrinsic module are not themselves intrinsic.

Access control: public, private and protected

Every identifier in a module has the public or private attribute (8.5.2). Without any statement the default is public, which exposes every helper variable and procedure to every use. The recommended style, used throughout heatflow, is to make the default private with a bare private statement and list the interface explicitly with public. Only one public or private statement without a list is allowed per module (C873).

There are three ways to set accessibility:

  • an attribute on a declaration: real(dp), private :: last_residual, or public/private on a type statement, which applies to the type name, not its components;

  • an accessibility statement with a list: public :: record_step, operator(.dot.), where the list may contain names and generic specifications such as assignment(=) and operator(+) (8.6.1);

  • a bare private or public, which sets the default for the whole specification part, including entities that are only use-associated into the module (see below).

The protected attribute (8.5.15) limits what other program units may do with a module variable or procedure pointer: they may read it, but they cannot define it, pass it as an actual argument to an intent(out) or intent(inout) dummy argument, or use it as the target of a pointer assignment (C859 and C860). Inside the module and its submodules the variable is assigned normally. That makes protected the way to expose read-only state without writing a getter function for every variable. The standard’s own example keeps a Celsius and a Fahrenheit value consistent by allowing writes only through a subroutine.

The next example combines all of this in a small module that keeps statistics for the heatflow solver loop. A protected public counter is readable by users, a private scalar and a private allocatable array are hidden state, and three different scoping units (the module procedures, an external subroutine and the main program) share that state.

module heatflow_stats
  use, intrinsic :: iso_fortran_env, only: real64, int32
  implicit none
  private                                  ! default accessibility for this module
  public :: record_step, report_stats      ! the public interface
  integer, parameter, public :: dp = real64

  integer(int32), protected, public :: steps_taken = 0_int32  ! readable, not writable outside
  real(dp), private :: last_residual = huge(1.0_dp)           ! hidden state
  real(dp), allocatable, private :: history(:)                ! hidden, allocated on first use

contains

  subroutine record_step(residual)
    real(dp), intent(in) :: residual
    steps_taken = steps_taken + 1_int32
    last_residual = residual
    if (.not. allocated(history)) allocate (history(0))
    history = [history, residual]          ! automatic reallocation on assignment
  end subroutine record_step

  subroutine report_stats()
    print '(a, i0, a, es10.3, a, i0)', 'steps=', steps_taken, '  last residual=', last_residual, &
      '  history size=', size(history)
  end subroutine report_stats

end module heatflow_stats

subroutine run_some_steps()
  use heatflow_stats, only: dp, record_step
  implicit none
  call record_step(1.0e-2_dp)
  call record_step(4.0e-3_dp)
end subroutine run_some_steps

program access_and_state
  use heatflow_stats, only: steps_taken, report_stats, record_step, dp
  implicit none
  external :: run_some_steps
  call record_step(5.0e-1_dp)
  call run_some_steps()                    ! another scoping unit sees the same module state
  call report_stats()
  print '(a, i0)', 'steps_taken read from the main program: ', steps_taken
end program access_and_state

Output:

steps=3  last residual= 4.000E-03  history size=3
steps_taken read from the main program: 3

The array history grows by the automatic reallocation of history = [history, residual] (see Allocatable Arrays and Dynamic Memory); it is private, so no other unit can resize or deallocate it. Three details of the access rules are worth remembering:

  • A bare private in a module also applies to entities that the module itself obtained by use association. In heatflow_stats the kinds real64 and int32 come from iso_fortran_env and are therefore private, which is why the module declares its own public dp instead of expecting users to import real64 through it.

  • An accessibility statement may name a procedure or type that is declared later in the same specification part. The attribute applies to the identifier, not to a position in the text.

  • A public procedure may have a private type as the type of a dummy argument or a function result. Callers can pass and store such values but cannot name the type; this is the usual way to make an opaque handle. Making the components of a type private is a separate question, covered in Derived Types.

GNU Fortran has the option -fmodule-private which makes every module’s default private as if each had a bare private; the Fortran option list of its manual documents it under the dialect options. Prefer writing private in the source: a program that depends on a compiler flag for its meaning breaks when it is built by another tool.

The following program is deliberately non-conforming. It shows the diagnostics GNU Fortran produces when the main program assigns to a protected variable and when it refers to a private entity of the module.

! INTENTIONALLY NON-CONFORMING: shows the diagnostics for misuse of PROTECTED and PRIVATE.
module heatflow_stats
  implicit none
  private
  public :: steps_taken
  integer, protected :: steps_taken = 0
  integer :: hidden = 7            ! private through the module default
end module heatflow_stats

program misuse
  use heatflow_stats
  implicit none
  steps_taken = 10                 ! error: protected, not definable here
  print *, hidden                  ! error: not accessible by use association
end program misuse

Diagnostics from gfortran -std=f2023 -Wall -Wextra -pedantic -fimplicit-none -fcheck=all, which stops at the first compilation error in the file:

02_protected_and_private_errors.f90:14:17:

   14 |   print *, hidden                  ! error: not accessible by use association
      |                 1
Error: Symbol 'hidden' at (1) has no IMPLICIT type
02_protected_and_private_errors.f90:13:3:

   13 |   steps_taken = 10                 ! error: protected, not definable here
      |   1~~~~~~~~~~~
Error: Variable 'steps_taken' is PROTECTED and cannot appear in a variable definition context (assignment) at (1)

The hidden diagnostic is the characteristic symptom of a private entity: since hidden is not accessible by use association, the compiler sees an undeclared name, and under implicit none the message is "no IMPLICIT type" rather than "private". Searching for that message is the first thing to do when a name that clearly exists in a module is not found. When a diagnostic of this kind appears in code that compiled before, check whether somebody added a bare private.

The use statement: only, renaming and operators

use (14.2.2) makes the public entities of a module accessible: named data objects, nonintrinsic types, procedures, abstract interfaces, generic identifiers and namelist groups. A use statement is itself a reference to the module: its public part must already be available when the statement is processed, which is why the module has to be compiled first. The general form has two spellings:

use [[, module-nature] ::] module-name [, rename-list]
use [[, module-nature] ::] module-name, only: [only-list]

A rename is local-name => use-name, and for defined operators operator(.local.) => operator(.used.). An entry of an only list is a generic specification (a generic name, operator(...) or assignment(=)), a plain name (which must be nongeneric, C1410), or a rename. Every name in the statement must be public in the module (C1409). A defined operator that is a type-bound generic cannot be renamed or listed in only (C1407, C1408); you get it by importing the type. Several use statements for the same module may appear in one scoping unit; if one has no only, all public entities are accessible, and otherwise the accessible set is the union of the only lists. A name may be imported twice under two different local names (Note 3 in 14.2.2).

The next example uses two modules that both export a procedure named prod with different meanings, and shows the three forms: no only (everything), only with a rename, and a renamed defined operator.

module stats_lib
  implicit none
  private
  public :: prod, operator(.dot.)
  interface operator(.dot.)
    module procedure dot_vec
  end interface
contains
  pure function prod(x) result(r)       ! product of all elements
    real, intent(in) :: x(:)
    real :: r
    r = product(x)
  end function prod
  pure function dot_vec(a, b) result(r)
    real, intent(in) :: a(:), b(:)
    real :: r
    r = dot_product(a, b)
  end function dot_vec
end module stats_lib

module math_lib
  implicit none
  private
  public :: prod, tau
  real, parameter :: tau = 6.2831855
contains
  pure function prod(a, b) result(r)    ! same name, different meaning
    real, intent(in) :: a, b
    real :: r
    r = a*b
  end function prod
end module math_lib

program use_only_rename
  use math_lib                          ! everything public: prod, tau
  use stats_lib, only: sprod => prod, operator(.inner.) => operator(.dot.)
  implicit none
  real :: v(3) = [1.0, 2.0, 3.0]
  print '(a, f0.1)', 'math_lib prod(2,3)   = ', prod(2.0, 3.0)
  print '(a, f0.1)', 'stats_lib sprod(v)   = ', sprod(v)
  print '(a, f0.1)', 'v .inner. v          = ', v .inner. v
  print '(a, f0.4)', 'tau                  = ', tau
end program use_only_rename

Output:

math_lib prod(2,3)   = 6.0
stats_lib sprod(v)   = 6.0
v .inner. v          = 14.0
tau                  = 6.2832

Notes on this example:

  • use math_lib has no only clause, so prod and tau are both accessible, and prod means the two-argument function. The entity from stats_lib is reachable only as sprod, because the only list imported it under that local name.

  • use stats_lib, only: sprod => prod imports exactly one entity of stats_lib; the .dot. operator is imported with the local name .inner.. The new operator name is a defined operator, written with dots and letters only.

  • The compiler does not report a conflict at the use statements. A name clash is an error only when the clashing identifier is referenced, as the next example shows. If the two names denote the same ultimate entity, for example because both modules obtained it from a third module, there is no clash at all.

GNU Fortran can warn about every use without only with -Wuse-without-only (it is not part of -Wall):

03_use_only_rename.f90:35:7:

   35 |   use math_lib                          ! everything public: prod, tau
      |       1
Warning: USE statement at (1) has no ONLY qualifier [-Wuse-without-only]

The following program is deliberately non-conforming: the two use statements without only are both legal, but the reference to prod is ambiguous.

! INTENTIONALLY NON-CONFORMING: two different entities with the same name are referenced.
module stats_lib
  implicit none
contains
  function prod(x) result(r)
    real, intent(in) :: x(:)
    real :: r
    r = product(x)
  end function prod
end module stats_lib

module math_lib
  implicit none
contains
  function prod(a, b) result(r)
    real, intent(in) :: a, b
    real :: r
    r = a*b
  end function prod
end module math_lib

program clash
  use stats_lib
  use math_lib                    ! legal: the clash only matters if the name is used
  implicit none
  print *, prod(2.0, 3.0)         ! error: ambiguous reference
end program clash

Diagnostic:

04_name_clash_error.f90:26:16:

   26 |   print *, prod(2.0, 3.0)         ! error: ambiguous reference
      |                1
Error: Name 'prod' at (1) is an ambiguous reference to 'prod' from module 'stats_lib'

The standard recommends only for a maintenance reason (C.10.2.2): with an only list every accessed entity names its source module in the unit that uses it, so nobody has to search other files to find where it is defined. It also keeps a change in an imported module, such as a new public name that collides with a local one, from silently changing a program that does not use it. The same annex warns against mixing use with implicit typing, because a new public name in the module can change the type of an implicitly typed variable; with implicit none everywhere this cannot happen.

Two more rules of use association matter in practice:

  • A use-associated variable is considered declared and a use-associated entity is considered defined, so a use has to precede the declarations that need it, and the local identifier of a use-associated entity cannot be given attributes again in the using unit. The exceptions are public/private, asynchronous and volatile (14.2.2). In particular a use-associated name cannot appear in a common or equivalence statement (Note 4).

  • Use association is transitive: if module c uses b and b uses a, what c can see of a depends on what b makes public. Entities of a that are not private in b are re-exported.

Re-exporting: facade modules

Because a bare private also governs use-associated names, a module can re-export a chosen subset of what it uses. A facade module gives users a single use heatflow while the project keeps small modules internally. The facade below imports two modules without only, makes everything private by default and then publishes a curated list; the kind ip is used internally but not exported.

module heatflow_kinds_demo
  use, intrinsic :: iso_fortran_env, only: real64, int32
  implicit none
  private
  public :: dp, ip
  integer, parameter :: dp = real64
  integer, parameter :: ip = int32
end module heatflow_kinds_demo

module heatflow_constants_demo
  use heatflow_kinds_demo, only: dp
  implicit none
  private
  public :: default_alpha
  real(dp), parameter :: default_alpha = 1.0_dp
end module heatflow_constants_demo

! The facade: users write "use heatflow_demo" and get a curated set of names.
module heatflow_demo
  use heatflow_kinds_demo                  ! brings dp, ip
  use heatflow_constants_demo              ! brings default_alpha
  implicit none
  private                                  ! default: nothing is re-exported ...
  public :: dp, default_alpha, describe     ! ... except what is listed (ip stays internal)
  integer(ip), parameter :: version = 1_ip
contains
  subroutine describe()
    print '(a, i0, a, f0.2)', 'facade v', version, ' default_alpha=', default_alpha
  end subroutine describe
end module heatflow_demo

program facade
  use heatflow_demo
  implicit none
  real(dp) :: alpha
  alpha = 2.0_dp*default_alpha
  call describe()
  print '(a, f0.2)', 'alpha = ', alpha
end program facade

Output:

facade v1 default_alpha=1.00
alpha = 2.00

The J3/23-007r1 text of 8.6.1 also lets an accessibility statement name a module (C874 to C876) so that, for example, private m1 makes everything accessed from m1 private while public :: m_type re-exports one type (Note 2 in 8.6.1). That form is not accepted by either installed compiler. The following fragment follows the idea of that note and is shown with the real diagnostics of GNU Fortran 16.2 and LLVM Flang 23; it is not a working program on those compilers, so use use m1, only: m_type followed by a plain private instead, as in the facade above.

! Re-export only one name from a module by making the module name itself private (see 8.6.1).
module m1
  implicit none
  integer, parameter :: m_type_kind = 4
  type :: m_type
    integer :: id = 0
  end type m_type
  integer :: other_var = 5
end module m1

module m2
  use m1
  implicit none
  private m1
  public :: m_type, make
contains
  function make(i) result(t)
    integer, intent(in) :: i
    type(m_type) :: t
    t%id = i + other_var          ! other_var is usable here, but is not re-exported
  end function make
end module m2

program reexport
  use m2
  implicit none
  type(m_type) :: t
  t = make(10)
  print '(a, i0)', 't%id = ', t%id
end program reexport
06b_private_module_name.f90:14:12:

   14 |   private m1
      |            1
Error: PRIVATE attribute applied to MODULE m1 at (1)
06b_private_module_name.f90:25:7:

   25 |   use m2
      |       1
Fatal Error: Cannot open module file 'm2.mod' for reading at (1): No such file or directory
compilation terminated.
error: Semantic errors in 06b_private_module_name.f90
06b_private_module_name.f90:14:11: error: No explicit type declared for 'm1'
    private m1
            ^^

Intrinsic and non-intrinsic modules

An intrinsic module is provided by the processor (iso_fortran_env, iso_c_binding, ieee_arithmetic, ieee_exceptions, ieee_features; see Intrinsic Modules and iso_fortran_env). A user can write a module with the same name as an intrinsic one, so the use statement takes an optional module nature (R1410):

  • use, intrinsic :: iso_fortran_env accesses the intrinsic module and it is an error if the name is not that of one (C1404);

  • use, non_intrinsic :: iso_fortran_env accesses the user’s module and it is an error if the name is not that of a nonintrinsic module (C1405);

  • a use with no nature accesses the nonintrinsic module when both exist, so a user module quietly shadows the intrinsic one;

  • one scoping unit cannot reference both an intrinsic and a nonintrinsic module of the same name (C1406).

Always write use, intrinsic :: for the standard modules, so that a stray iso_fortran_env.f90 in a vendored library cannot change the meaning of the program. The example defines a user module that shadows iso_fortran_env and then shows each nature in its own scoping unit.

! A user module that happens to share its name with an intrinsic module.
module iso_fortran_env
  implicit none
  integer, parameter :: dp = kind(1.0d0)
  character(len=*), parameter :: origin = 'user module'
end module iso_fortran_env

subroutine with_intrinsic()
  use, intrinsic :: iso_fortran_env, only: real64, compiler_version
  implicit none
  print '(a, i0)', 'intrinsic: real64 = ', real64
  print '(a)', 'intrinsic: compiler_version() is available: ' // merge('yes', 'no ', len(compiler_version()) > 0)
end subroutine with_intrinsic

program nature
  use, non_intrinsic :: iso_fortran_env, only: dp, origin
  implicit none
  external :: with_intrinsic
  print '(a, i0, 2a)', 'non_intrinsic: dp = ', dp, ', origin = ', origin
  call with_intrinsic()
end program nature

Output:

non_intrinsic: dp = 8, origin = user module
intrinsic: real64 = 8
intrinsic: compiler_version() is available: yes

The -fintrinsic-modules-path dir option of GNU Fortran (and LLVM Flang, which has the same spelling) says where the compiler’s own pre-compiled intrinsic module files are found, which matters only for unusual installations or cross compilers. The nature keywords arrived with Fortran 2003, together with the intrinsic modules iso_fortran_env and iso_c_binding.

Module variables, save and common blocks

A variable declared in the specification part of a module is a global variable with a controlled name: every program unit that uses the module sees the same storage. Since Fortran 2008 the standard says plainly that a variable declared in the scoping unit of a main program, module or submodule implicitly has the save attribute (8.5.16), so it keeps its value, its allocation status and its association status for the whole execution, however many times the procedures that use it are entered. Writing save explicitly is permitted and harmless; it was needed in older code where a module variable could become undefined when no active unit referenced the module. In heatflow_stats above, steps_taken, last_residual and history persist across the three scoping units that call record_step.

Module variables replace the common block (8.10.2), which shares storage by position instead of by name and type, requires the same layout in every unit that uses it and can silently misalign. A named common block is covered only in Legacy Features and Modernization. The comparison favours modules on every point:

Aspect Module variable common block

Identity

shared by name and type, checked by the compiler

shared by storage position; layouts can disagree between units

Initialization

a declaration initializer

only in a block data program unit

Accessibility

public, private, protected

none

Allocatable, pointer, derived type

any

restricted: allocatable objects are not allowed, and protected cannot apply (C858)

Interoperability

bind(c) variables, 8.5.5

bind(c) common blocks, which also imply save

Global mutable state still has costs: it hides dependencies between procedures, it makes a procedure impure, and it makes concurrent use unsafe, because a procedure that assigns to a module variable cannot be pure and cannot be called from a do concurrent body that has no locality specifier for it (see Procedures, Functions and Subroutines). Prefer module constants (parameter) and pass state through derived types such as grid_t and solver_t; keep module variables for genuinely global quantities, such as a logging unit or a counter that is read through a protected variable.

A protected pointer may not have its association changed outside the module (C860), but nothing stops a user from modifying the target through the pointer unless the target is itself protected; the attribute protects the association, not the whole data structure behind it.

Submodules and separate module procedures

A module that contains its procedure bodies has a drawback that grows with the project: the compiler writes information about the module for its users, and a build tool that treats that file as a dependency has to decide when its users must be compiled again. The cure is to separate interface from implementation:

  • the module declares the interface of a separate module procedure with an interface body that begins with the keyword module (15.4.3.2, 15.6.2.5);

  • a submodule (14.2.3), submodule (parent) name, contains the bodies: either with the whole header repeated, module subroutine smooth(...), or with only module procedure smooth, in which case the characteristics come from the interface body (15.6.2.5);

  • users use the module and never see the submodule. Only the interface body needs to be public (15.4.3.2): a separate module procedure is accessible by use association if and only if its interface body is declared in the specification part of a module and is public.

A submodule has a host, given by the parent-identifier (R1418): submodule (heatflow_smooth) extends a module, and submodule (heatflow_smooth:smooth_impl) extends the submodule smooth_impl of that module. Together they form a tree with a module at the root (14.2.3). The submodule name by itself is not a global identifier: the identifier is the ordered pair (ancestor module, submodule), so two different modules can each have a submodule named impl. A submodule accesses its host’s entities, including the host’s private ones, by host association; it may declare more separate module procedure interfaces, which are then visible only to it and its descendants.

flowchart TD A["module heatflow_smooth
interfaces: smooth, grid_energy
private variable: calls"] --> B["submodule smooth_impl
bodies of smooth and grid_energy
declares interface of sweep"] B --> C["submodule smooth_kernel
body of sweep"] D["program use_smooth"] -.->|"use heatflow_smooth"| A

The example below is the smallest realistic version of the heatflow solver split: a module with two public separate module procedures and a private counter, a submodule that implements both (one with the full header and one with module procedure), declares a third interface sweep and increments the counter by host association, and a submodule of that submodule that implements sweep. A main program uses the module. The four files are compiled in this order and linked:

module heatflow_smooth
  use, intrinsic :: iso_fortran_env, only: real64, int32
  implicit none
  private
  public :: dp, smooth, grid_energy, sweeps_done

  integer, parameter :: dp = real64
  integer(int32) :: calls = 0_int32          ! private: only this module and its submodules see it

  interface
    ! Separate module procedures: interface here, bodies in submodules.
    module subroutine smooth(u, nsweeps, residual)
      real(dp), intent(inout) :: u(:, :)
      integer, intent(in) :: nsweeps
      real(dp), intent(out) :: residual
    end subroutine smooth

    module function grid_energy(u) result(e)
      real(dp), intent(in) :: u(:, :)
      real(dp) :: e
    end function grid_energy
  end interface

contains

  integer function sweeps_done()
    sweeps_done = calls
  end function sweeps_done

end module heatflow_smooth
submodule (heatflow_smooth) smooth_impl
  implicit none

  interface
    ! Declared in this submodule, so it is private to it and to its descendants.
    module subroutine sweep(u, unew, residual)
      real(dp), intent(in) :: u(:, :)
      real(dp), intent(out) :: unew(:, :)
      real(dp), intent(out) :: residual
    end subroutine sweep
  end interface

contains

  module subroutine smooth(u, nsweeps, residual)     ! full header repeated
    real(dp), intent(inout) :: u(:, :)
    integer, intent(in) :: nsweeps
    real(dp), intent(out) :: residual
    real(dp), allocatable :: unew(:, :)
    integer :: k
    allocate (unew, mold=u)
    residual = 0.0_dp
    do k = 1, nsweeps
      call sweep(u, unew, residual)
      u(2:size(u, 1) - 1, 2:size(u, 2) - 1) = unew(2:size(u, 1) - 1, 2:size(u, 2) - 1)
      calls = calls + 1                               ! host association reaches the private variable
    end do
  end subroutine smooth

  module procedure grid_energy                         ! header omitted: taken from the interface body
    e = sum(u*u)
  end procedure grid_energy

end submodule smooth_impl
submodule (heatflow_smooth:smooth_impl) smooth_kernel
  implicit none
contains

  module subroutine sweep(u, unew, residual)
    real(dp), intent(in) :: u(:, :)
    real(dp), intent(out) :: unew(:, :)
    real(dp), intent(out) :: residual
    integer :: i, j
    unew = u
    residual = 0.0_dp
    do j = 2, size(u, 2) - 1
      do i = 2, size(u, 1) - 1
        unew(i, j) = 0.25_dp*(u(i - 1, j) + u(i + 1, j) + u(i, j - 1) + u(i, j + 1))
        residual = max(residual, abs(unew(i, j) - u(i, j)))
      end do
    end do
  end subroutine sweep

end submodule smooth_kernel
program use_smooth
  use heatflow_smooth, only: dp, smooth, grid_energy, sweeps_done
  implicit none
  real(dp) :: u(5, 5), residual
  u = 0.0_dp
  u(3, 3) = 100.0_dp
  print '(a, f10.3)', 'energy before = ', grid_energy(u)
  call smooth(u, 10, residual)
  print '(a, f10.3, a, es10.3)', 'energy after  = ', grid_energy(u), '  residual = ', residual
  print '(a, i0)', 'sweeps done   = ', sweeps_done()
end program use_smooth

Build and run (GNU Fortran 16.2, flags as for every example):

gfortran -std=f2023 -Wall -Wextra -pedantic -fimplicit-none -fcheck=all -c 08a_heatflow_smooth.f90
gfortran -std=f2023 -Wall -Wextra -pedantic -fimplicit-none -fcheck=all -c 08b_smooth_impl.f90
gfortran -std=f2023 -Wall -Wextra -pedantic -fimplicit-none -fcheck=all -c 08c_smooth_kernel.f90
gfortran -std=f2023 -Wall -Wextra -pedantic -fimplicit-none -fcheck=all -c 08d_main.f90
gfortran 08a_heatflow_smooth.o 08b_smooth_impl.o 08c_smooth_kernel.o 08d_main.o -o smooth
./smooth
energy before =  10000.000
energy after  =      4.883  residual =  1.562E+00
sweeps done   = 10

LLVM Flang 23 builds the same four files in the same order and prints the same three lines.

The rules that the example demonstrates, and the ones it does not:

  • Order of compilation. The module first, then the submodule that extends it, then its descendants. A program unit that only refers to the module with a use statement needs the module’s file, not the submodules' files.

  • No import in module procedure interfaces. The interface body of a module procedure sees the module’s names (dp above) by host association. GNU Fortran rejects an import there with "IMPORT statement at (1) is not permitted in a module procedure interface body".

  • The body may shorten the header. module procedure grid_energy takes the arguments, the function result name (e) and the attributes from the interface body, and the result variable is declared in the interface, not in the body. With the full header, the characteristics must agree with the interface body, and the compiler checks them (see the error example below). A function keeps the result name declared in the interface body.

  • non_recursive, pure and elemental. A separate module procedure is non_recursive if and only if the interface body says so. Prefixes such as pure and elemental have to be consistent between the interface body and a full header; the cascade example later writes pure module function in both.

  • No second definition. A separate module procedure cannot be defined more than once (15.6.2.5), and the procedure-name of end procedure must match.

  • An interface body without a matching body can be used only to give an explicit interface, not to call the procedure (15.4.3.2), and in practice the call fails at link time.

  • Statement functions, entry and format statements may not appear in a submodule specification part (C1411), as in a module.

  • Submodule objects must be linked. A submodule’s object file has to be given to the linker explicitly. Leaving out the object of 08b_smooth_impl.f90 and 08c_smooth_kernel.f90, which hold all the bodies, gives:

/usr/bin/ld: 08d_main.o: in function `MAIN__':
08d_main.f90:7:(.text+0x150): undefined reference to `__heatflow_smooth_MOD_grid_energy'
/usr/bin/ld: 08d_main.f90:8:(.text+0x1f0): undefined reference to `__heatflow_smooth_MOD_smooth'

(The 08b object alone is not enough either: it refers to sweep, which is defined in the object of the submodule of the submodule, so the link fails with an undefined reference to __heatflow_smooth.smooth_impl_MOD_sweep.)

The following program is deliberately non-conforming: the submodule’s header disagrees with the interface body.

! INTENTIONALLY NON-CONFORMING: the submodule body disagrees with the interface body in the module.
module heatflow_scale
  implicit none
  interface
    module function scaled(x, factor) result(r)
      real, intent(in) :: x(:)
      real, intent(in) :: factor
      real :: r(size(x))
    end function scaled
  end interface
end module heatflow_scale

submodule (heatflow_scale) heatflow_scale_impl
  implicit none
contains
  module function scaled(x, factor) result(r)
    real, intent(in) :: x(:)
    integer, intent(in) :: factor          ! error: integer, the interface says real
    real :: r(size(x))
    r = x*real(factor)
  end function scaled
end submodule heatflow_scale_impl

Diagnostics:

07_interface_mismatch_error.f90:16:29:

   16 |   module function scaled(x, factor) result(r)
      |                             1~~~~~
Error: Type mismatch in argument 'factor' (INTEGER(4)/REAL(4)) at (1)

LLVM Flang is more explicit:

error: Semantic errors in 07_interface_mismatch_error.f90
07_interface_mismatch_error.f90:18:28: error: Dummy argument 'factor' has type INTEGER(4); the corresponding argument in the interface body has distinct type REAL(4)
      integer, intent(in) :: factor          ! error: integer, the interface says real
                             ^^^^^^
07_interface_mismatch_error.f90:7:27: Declaration of 'factor'
        real, intent(in) :: factor
                            ^^^^^^

When the full header is repeated, changing a signature means editing two places. This duplication is the price of the firewall between interface and implementation; keep interfaces small and stable.

Breaking cyclic dependencies with submodules

A module may not reference itself, directly or indirectly (14.2.2), so two modules that each need the other at specification level cannot exist. This program is deliberately non-conforming:

! INTENTIONALLY NON-CONFORMING: two modules that use each other (14.2.2 forbids this).
module grid_m
  use solver_m, only: solver_t
  implicit none
  type :: grid_t
    integer :: nx = 0
  end type grid_t
end module grid_m

module solver_m
  use grid_m, only: grid_t
  implicit none
  type :: solver_t
    type(grid_t) :: grid
  end type solver_t
end module solver_m

program cycle
  use solver_m
  implicit none
end program cycle
11_cyclic_use_error.f90:3:7:

    3 |   use solver_m, only: solver_t
      |       1
Fatal Error: Cannot open module file 'solver_m.mod' for reading at (1): No such file or directory
compilation terminated.

LLVM Flang, which processes the whole file in dependency order, reports the cycle by name:

error: Semantic errors in 11_cyclic_use_error.f90
11_cyclic_use_error.f90:2:8: error: Some modules in this compilation unit form one or more cycles of dependence
  module grid_m
         ^^^^^^
11_cyclic_use_error.f90:10:8: Module in a cycle
  module solver_m
         ^^^^^^^^

GNU Fortran only reports that a module file is missing, because it handles each module in the order of the source file. The way out is to notice that a cycle often exists between implementations, not between interfaces. If grid_m needs the solver only in the body of one procedure, keep the interface of that procedure in grid_m and put the body, which does use solver_m, in a submodule of grid_m. The dependency graph becomes grid_m, then solver_m, then grid_impl, with no cycle:

module grid_m
  use, intrinsic :: iso_fortran_env, only: real64
  implicit none
  private
  public :: dp, grid_t, summarize

  integer, parameter :: dp = real64

  type :: grid_t
    integer :: nx = 0, ny = 0
    real(dp), allocatable :: u(:, :)
  end type grid_t

  interface
    ! The interface does not mention solver_m, so grid_m itself never needs it.
    module function summarize(g) result(text)
      type(grid_t), intent(in) :: g
      character(len=:), allocatable :: text
    end function summarize
  end interface
end module grid_m
module solver_m
  use grid_m, only: dp, grid_t
  implicit none
  private
  public :: solver_t, max_value

  type :: solver_t
    type(grid_t) :: grid
  end type solver_t

contains

  function max_value(s) result(m)
    type(solver_t), intent(in) :: s
    real(dp) :: m
    m = maxval(s%grid%u)
  end function max_value
end module solver_m
submodule (grid_m) grid_impl
  use solver_m, only: solver_t, max_value       ! grid_m's body depends on solver_m: legal here
  implicit none
contains
  module function summarize(g) result(text)
    type(grid_t), intent(in) :: g
    character(len=:), allocatable :: text
    type(solver_t) :: s
    character(len=32) :: buf
    s%grid = g
    write (buf, '(a, i0, a, i0, a, f0.1)') 'grid ', g%nx, 'x', g%ny, ' max=', max_value(s)
    text = trim(buf)
  end function summarize
end submodule grid_impl
program break_cycle
  use grid_m, only: dp, grid_t, summarize
  implicit none
  type(grid_t) :: g
  g%nx = 3
  g%ny = 2
  allocate (g%u(g%nx, g%ny), source=1.5_dp)
  g%u(2, 2) = 7.5_dp
  print '(a)', summarize(g)
end program break_cycle

The files are compiled in the order 10a_grid_m.f90, 10b_solver_m.f90, 10c_grid_impl.f90, 10d_main.f90 with the usual flags, linked and run:

grid 3x2 max=7.5

Module files: .mod and .smod

Compiling a module (or submodule) writes a module file beside the object file. A later use (or submodule) reads it instead of the source: the standard calls this dependent compilation and says the exact way the public parts of a module are made available is processor dependent (C.10.2). So the module file format, its suffix and what it contains are decisions of the compiler, and they differ.

After the build above, GNU Fortran 16.2 leaves these files:

heatflow_smooth.mod
heatflow_smooth.smod
heatflow_smooth@smooth_impl.smod
heatflow_smooth@smooth_kernel.smod

heatflow_smooth.mod carries what other units need; heatflow_smooth.smod and the heatflow_smooth@smooth_impl.smod files carry what the submodules need, named ancestor@submodule. The .mod file is a gzip-compressed text file whose first line identifies the compiler’s module format, and it is not meant to be read by people:

GFORTRAN module version '16' created from 08a_heatflow_smooth.f90

LLVM Flang 23, in contrast, writes readable Fortran source: a .mod file for the module and one for each submodule, named heatflow_smooth-smooth_impl.mod, without a separate suffix. The module file of the example is, in full:

!mod$ v1 sum:54a013937ca69c50
!need$ fa6e749ea5254d1b i iso_fortran_env
module heatflow_smooth
use,intrinsic::iso_fortran_env,only:real64
use,intrinsic::iso_fortran_env,only:int32
private::real64
private::int32
integer(4),parameter::dp=8_4
integer(4),private::calls
interface
module subroutine smooth(u,nsweeps,residual)
real(8),intent(inout)::u(:,:)
integer(4),intent(in)::nsweeps
real(8),intent(out)::residual
end
end interface
interface
module function grid_energy(u) result(e)
real(8),intent(in)::u(:,:)
real(8)::e
end
end interface
contains
function sweeps_done()
integer(4)::sweeps_done
end
end

The consequences for builds are the reason a documentation page about the language has to mention them:

  • Not portable. A .mod or .smod file made by one compiler (or by an earlier major version of the same one: gfortran’s header carries a format version) cannot be read by another. Libraries distributed as .a plus .mod files are tied to the compiler that built them; a Fortran library that is to be used from several compilers is distributed as source.

  • Where they go and where they are searched. GNU Fortran writes module files in the current directory unless -Jdir is given, and -Jdir is also searched by use; -Idir adds search directories. LLVM Flang spells the output directory -module-dir dir or -J dir, and -I dir for the search; with -module-dir it did not create a missing directory and failed with "Error writing …​ No such file or directory". Build tools set these options for you; see Compilers and Compiler Options.

  • Name mangling. The compiler derives linker symbol names from the module name: nm of the objects above shows __heatflow_smooth_MOD_smooth and __heatflow_smooth_MOD_grid_energy, and for the procedure declared inside the submodule __heatflow_smooth.smooth_impl_MOD_sweep. Other compilers use different mangling, which is why Fortran code that must be called from C uses bind(c) (see Interoperability with C).

  • Same name, same program. Two modules with the same name cannot be linked into one program, because the global identifier would clash, and two different sources writing grid.mod into one directory overwrite each other.

Compilation order and recompilation cascades

The compile order follows the use and submodule statements: a module before its users, a module before its submodules, a submodule before its descendants. The build tools find this order by scanning the sources; make needs explicit rules (or a generator of them), CMake and Meson scan automatically, and fpm does it with no configuration, as described in the build tools page and the fpm page.

Compilation order of heatflow with and without submodules. Without submodules the solver module holds interfaces and bodies, so editing a Jacobi body recompiles heatflow_solver and the application. With submodules the solver module keeps only interfaces and the Jacobi and FTCS bodies live in submodules, so the same edit recompiles only the heatflow_jacobi submodule before the link step

Dependent compilation has a cost that is invisible in a small project. If a program unit depends on the module file, a change that alters the module file forces every user to be compiled again, and in a build that depends on the object file of the module, any edit of the module’s source does. In a large simulation code a module with hundreds of procedures at the root of a deep dependency tree turns a one-line change into a complete rebuild: the "recompilation cascade". Submodules move bodies out of the file that everybody depends on.

The script below measures it. It builds the same tiny program four ways: with the function inside the module (mono) and with its body in a submodule (split), each with two typical dependency styles in a Makefile: the user depends on the module’s object file (obj) or on its module file (mod). After each of three edits it prints which source files make compiled.

#!/bin/sh
# What does a dependency-driven build recompile after three edits, with and without a submodule?
# Style "obj": main.o depends on the object file of the module (typical hand-written Makefile).
# Style "mod": main.o depends on the module file only (what CMake and fpm effectively do).
set -e
FFLAGS="-std=f2023 -Wall -Wextra -pedantic"

mono_src() { cat <<'F90'
module calc
  implicit none
  private
  public :: mean_square
contains
  pure function mean_square(x) result(r)
    real, intent(in) :: x(:)
    real :: r
    r = sum(x*x)/real(size(x))
  end function mean_square
end module calc
F90
}
split_src() { cat <<'F90'
module calc
  implicit none
  private
  public :: mean_square
  interface
    pure module function mean_square(x) result(r)
      real, intent(in) :: x(:)
      real :: r
    end function mean_square
  end interface
end module calc
F90
cat > calc_impl.f90 <<'F90'
submodule (calc) calc_impl
  implicit none
contains
  pure module function mean_square(x) result(r)
    real, intent(in) :: x(:)
    real :: r
    r = sum(x*x)/real(size(x))
  end function mean_square
end submodule calc_impl
F90
}
main_src() { cat <<'F90'
program main
  use calc, only: mean_square
  implicit none
  print '(f0.2)', mean_square([1.0, 2.0, 3.0])
end program main
F90
}

makefile() {   # $1 = mono|split   $2 = obj|mod
  echo "FC = gfortran"; echo "FFLAGS = $FFLAGS"
  if [ "$1" = mono ]; then
    echo "app: calc.o main.o"; printf '\t$(FC) -o $@ $^\n'
    echo "calc.o calc.mod &: calc.f90"; printf '\t$(FC) $(FFLAGS) -c calc.f90\n'
    if [ "$2" = obj ]; then echo "main.o: main.f90 calc.o"; else echo "main.o: main.f90 calc.mod"; fi
  else
    echo "app: calc.o calc_impl.o main.o"; printf '\t$(FC) -o $@ $^\n'
    echo "calc.o calc.mod calc.smod &: calc.f90"; printf '\t$(FC) $(FFLAGS) -c calc.f90\n'
    echo "calc_impl.o: calc_impl.f90 calc.smod"; printf '\t$(FC) $(FFLAGS) -c calc_impl.f90\n'
    if [ "$2" = obj ]; then echo "main.o: main.f90 calc.o"; else echo "main.o: main.f90 calc.mod"; fi
  fi
  printf 'main.o:\n\t$(FC) $(FFLAGS) -c main.f90\n'
}

recompiled() { make 2>&1 | sed -n 's/^gfortran .* -c \(.*\.f90\)$/\1/p' | tr '\n' ' '; echo; }
tick() { sleep 1.1; }

for style in obj mod; do
for v in mono split; do
  d=/tmp/casc/$style-$v; rm -rf $d; mkdir -p $d; cd $d
  if [ $v = mono ]; then mono_src > calc.f90; impl=calc.f90; else split_src > calc.f90; impl=calc_impl.f90; fi
  main_src > main.f90; makefile $v $style > Makefile
  make >/dev/null 2>&1
  printf '%-10s' "$style/$v"
  tick; sed -i 's|sum(x\*x)/real(size(x))|sum(x*x)/real(size(x)) + 0.0|' $impl
  printf ' edit 1 (body): '; recompiled | tr -d '\n'
  tick; sed -i 's|^contains|contains\n  pure function helper(x) result(r)\n    real, intent(in) :: x\n    real :: r\n    r = x*x\n  end function helper|' $impl
  printf '| edit 2 (private helper): '; recompiled | tr -d '\n'
  tick
  sed -i 's|public :: mean_square|public :: mean_square, nothing|' calc.f90
  if [ $v = mono ]; then
    sed -i 's|^contains|contains\n  subroutine nothing()\n  end subroutine nothing|' calc.f90
  else
    sed -i 's|^  end interface|    module subroutine nothing()\n    end subroutine nothing\n  end interface|' calc.f90
    sed -i 's|^contains|contains\n  module subroutine nothing()\n  end subroutine nothing|' calc_impl.f90
  fi
  printf '| edit 3 (public procedure): '; recompiled
done; done

Run in the container with GNU Make 4.4.1 and GNU Fortran 16.2:

obj/mono   edit 1 (body): calc.f90 main.f90 | edit 2 (private helper): calc.f90 main.f90 | edit 3 (public procedure): calc.f90 main.f90
obj/split  edit 1 (body): calc_impl.f90 | edit 2 (private helper): calc_impl.f90 | edit 3 (public procedure): calc.f90 calc_impl.f90 main.f90
mod/mono   edit 1 (body): calc.f90 | edit 2 (private helper): calc.f90 | edit 3 (public procedure): calc.f90 main.f90
mod/split  edit 1 (body): calc_impl.f90 | edit 2 (private helper): calc_impl.f90 | edit 3 (public procedure): calc.f90 calc_impl.f90 main.f90

What this shows:

  • With the object-file style (the style of many hand-written Makefiles), every edit to the monolithic module recompiles its users, whereas with a submodule only the submodule is compiled until the interface changes (edit 3).

  • With the module-file style, GNU Fortran 16.2 did not rewrite calc.mod for edits 1 and 2: a change to a body or to a private helper left the module file untouched, and make did not recompile main.f90. That is a property of the compiler, not of the language: the standard only observes that a processor could recognise changes that do not affect users (C.10.2.3), and GNU Fortran 16.2 evidently does. A compiler that always rewrites the module file triggers the cascade with module-file rules, too. Submodules give the same protection by construction, on every compiler.

  • An interface change still cascades (edit 3 in all four rows): adding a public procedure changes what users can see, so they must be compiled again. Submodules reduce the reasons for a cascade; they do not remove dependent compilation.

  • In the split build the submodule’s calc_impl.o has to be linked; and calc_impl.f90 depends on calc.smod.

The measurements are for these small files and one compiler; read them as a demonstration of the dependency structure, not as a benchmark of compile times. When the build uses CMake, Meson or fpm, the dependency scanner decides what a change recompiles, so check its behaviour on your own project.

Where the books are outdated

The two Fortran books consulted for this section cover modules and submodules at the concept level and are still a sound introduction. Where they differ from the current practice:

Area Outdated in the books, current approach

Build tools

the submodule chapter motivates submodules with recompilation cascades under make and says nothing about tools that scan modules; today fpm, CMake and Meson find the order and the dependencies, and the cascade measured above depends on the style of dependency rule

Compiler context

no compiler-support information for submodules; GNU Fortran 16.2 and LLVM Flang 23 both build the submodule examples on this page

save

some older teaching material says a module variable needs save to persist; in Fortran 2008 and later it has the attribute implicitly, and an explicit save only documents the intent

common

common and block data as the way to share data are legacy practice, kept for reading old code

Standard version provenance and compiler support

Feature Introduced Compiler support checked on 2026-10-18

Modules, use, only, renaming, public/private, contains

Fortran 90

all compilers

protected, use, intrinsic, use, non_intrinsic

Fortran 2003

GNU Fortran 16.2 and LLVM Flang 23 accept the examples

Submodules and separate module procedures

Fortran 2008

GNU Fortran 16.2 and LLVM Flang 23 build the examples

Implicit save stated for module variables, empty contains

Fortran 2008

both

Accessibility statement naming a module (private m1)

text of Fortran 2023, 8.6.1

rejected by GNU Fortran 16.2 and LLVM Flang 23

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

  • A name appears in two modules and you wrote use for both with no only. The program compiles until the name is used. Use only, or rename.

  • A bare private hides a kind or type that users need. Re-export with an explicit public statement.

  • protected does not make data immutable. It stops assignment and pointer assignment outside the module, but not writes through a protected pointer to its target.

  • A module variable is shared state. It is global, saved, not thread-safe and makes procedures impure.

  • A submodule object file is not in the library. If the link fails with an undefined reference to a separate module procedure, the submodule was not compiled, not added to the archive, or not linked.

  • Signatures are written twice. Keep interface body and header in sync; use module procedure name in the submodule to avoid repeating them.

  • A change of .mod format between compiler versions invalidates old module files; rebuild everything after upgrading the compiler.

  • A user module named like an intrinsic module shadows the intrinsic one in a use without nature.

  • import is rejected in module procedure interface bodies. They see the module by host association.

  • Two modules with the same name in two libraries collide at link time and in the module directory.

  • Module files from different compilers in one directory overwrite each other or fail to read; give each compiler its own module directory.