Fortran Style Guide and Best Practices
February 18, 2025 · View on GitHub
Adopting a consistent style can improve code legibility through the choice of good naming conventions.
This allows code review discussions to focus on semantics and substance rather than pedantry. Consistent whitespace usage, and not polluting line endings with trailing white space makes git diffs considerably more legible. This style guide is a living document and proposed changes may be adopted after discussing them and coming to a consensus.
This guide is fairly opinionated but it adopts much from Fortran community's stdlib project, their Best Practices document, as well as Python's style guide, PEP8.
To enforce a consistent style we will employ fprettify as the tool of choice (until something better comes along). Style checks should be run to flag any severe non-conformance. pre-commit can be configured to run a number of checks as well as fprettify on every git commit or push (see pre-commit config). Using CI to force these checks is also a future possibility.
A note on fortran-lang.org's Best Practices document:
This document is the best outside resource in terms of Modern Fortran Best Practices as it has been molded by all experiences and consensus building on Fortran community's discourse. Read it and use it shamelessly; rules that contradict the Best Practices document in this STYLE_GUIDE should not be mindlessly created or used.
Table of Contents
- Use (modern) standard Fortran
- File naming conventions
- Variable and procedure naming
- Indentation & whitespace
- Attributes
- Scope block end closing statements
- Comments
- Dead Code
- Precision and Kinds
- Common pitfalls
- Enums
- MPI
- Document public API code with FORD
- Error Handling
- WIP
- References
Use (modern) standard Fortran
Modern Fortran is slowly being updated. Some features are being obsoleted and should not be used for active Fortran development. Starting from Fortran 2018 standards, we can work on moving to more modern standards like Fortran 2023. Standards can be enforced by the compiler. For example, in gfortran, compile with -std=f2018 to enforce Fortran 2018 compliance and flag obsolescent features.
- Always use
implicit none (type, external)at the start of the parent scope, most commonlyprogramandmodule. This is a Fortran 2018 feature, See this discussion on reasoning.implicit noneis redundant everywhere else (notable exception: externally described interfaces)
- Do not use obsolescent or deleted language features. E.g.,
common,pause,entry, arithmeticifand computedgoto - Do not use vendor extensions in the form of non-standard syntax and vendor supplied intrinsic procedures. E.g.,
real*8oretime(), GNU or Intel extensions. - Use the modern operators
==,<,>,/=,<=,=>instead of the old.eq.,.lt.,.gt.. The logical.eqv.and.neqv.are still the correct operators to use - Modules should make all their attributes
privateby default - Modules should make attributes
publiconly when they need to be accessed outside the scope - Module use should fully qualify all objects imported from it.
use mpi, only: MPI_STATUS_SIZE, MPI_Send, MPI_Recv, MPI_COMM_WORLD, MPI_REAL4 - Modules should be used to house all functions and subroutines of the project. There should be none without an interface (which the module provides) except third-party libraries.
File naming conventions
- Source files should contain at most one
program,module, orsubmodule - The filename should match the program or module name and have the file extension
.f90or.F90if preprocessing is required (e.g.,#ifdef MPIblocks) - Lowercase should be preferred in both filenames and program or module names
- If the interface and implementation is split using submodules the implementation submodule file should have the same name as the interface (parent) module but end in
_implementation. E.g.,string_class.f90andstring_class_implementation.f90 - Tests should be added in the
testsubdirectory and have the same name as the module they are testing with thetest_prefix added. E.g.,string_class.f90andtest/test_string_class.f90. Consider utilizing the same subdirectory structure assrc/
Source file directory structure
- Drivers (referring to
programfiles) should be placed underapp/ - Module files should be placed under
src/. - Module files that belong to a singular theme could be grouped together in subdirectories. Examples include
src/mpi/,src/lbm_solver/,src/ca_solver/ - Test files should be placed under
test/. When the number of tests increases, consider grouping them together in the same subdirectory structure as module files insrc/
Variable and procedure naming
- Variable and procedure names, as well as Fortran keywords, should be written in lowercase. An exception could be made for
MPIorOMProutines but that is harder to enforce with a formatter. - Named constants should be written in all UPPERCASE, separated by underscores if needed, as per PEP8. Exceptions include mathematical constants, or domain-specific terms. For example:
integer, parameter :: PERIODIC = 0, DIRICHLET = 1, NEUMANN = 2 character(*), parameter :: MODULE_NAME = "ca_solver" real(sp), parameter :: pi = 3.141592653589793_sp ! typically in lowercase real(sp) :: Re ! Reynolds number - very easy to spot - Variable and procedure names should be made up of one or more full words separated by an underscore, for example
has_failedis preferred overhasfailed - Where conventional and appropriate shortening of a word is used then the underscore may be omitted, for example
linspaceis preferred overlin_space - Subroutine and function naming should be descriptive.
- Avoid abbreviations unless they are widely understood or referenced. For example
caandlbmwill be heavily referenced in code and materials, butbgk,trt,strak,SMS,DD_Advectionare extremely hard to parse - Use a prefix like
apply_,compute_,initialize_, orwrite_to indicate the action. Compare:
andcall CA_METHOD() ! vs call advance_solidification() ! or call perform_solidification_step()call OCTA_CAPTURE() ! vs call capture_adjacent_cells() - Avoid abbreviations unless they are widely understood or referenced. For example
- Logical variables/functions should read like yes/no questions. Use prefixes like
is_,has_, orshould_:logical :: is_converged, has_boundary_condition function should_terminate() result(terminate)
Module naming - a deeper dive
Module names should strike a balance between being descriptive and concise. They should clearly convey the module's purpose without being overly verbose or cryptic. A good module name should answer the question: What does this module do?
When naming modules, imagine you are a new developer joining the project with limited familiarity with the codebase. Would the module name give you a clear idea of its functionality? For example, consider the following module names:
use UPB1
use lbsubs
These names are overly reductionist and provide no meaningful context. What is UPB1? What does lbsubs do? Without additional documentation, these names are unhelpful and create unnecessary cognitive overhead.
Instead, aim for module names that are self-explanatory and aligned with their purpose:
use lib_mpi_halo ! Handles MPI halo exchanges
use boundary_conditions ! Manages boundary condition application
use lbm_solver_core ! Core routines for the Lattice Boltzmann solver
Guidelines for module naming
- Use Full Words: Avoid abbreviations unless they are widely understood (e.g.,
mpi,lbm,lib). - Be Specific: The name should reflect the module's primary responsibility.
- Bad:
solver(too vague) - Good:
fluid_solver,solidification_solver
- Bad:
- Use Prefixes for Grouping: Prefixes can help organize related modules.
- Example:
lib_mpi_halo,lib_io_helpers
- Example:
- Avoid Jargon: Use terminology that is familiar to the domain but accessible to newcomers.
- Bad:
bgk_collision(unlessbgkis universally understood in your context) - Good:
collision_operatororkinetic_collision
- Bad:
- Keep It Concise: While being descriptive, avoid overly long names.
- Bad:
module_for_handling_boundary_conditions_in_3d_simulations - Good:
boundary_conditions
- Bad:
Examples of module names
| Purpose | Poor Name | Better Name |
|---|---|---|
| MPI halo exchange utilities | halo | lib_mpi_halo |
| Boundary condition management | bc | boundary_conditions |
| Lattice Boltzmann solver core | lbsolver | lbm_solver_core |
| Input/output utilities | io | lib_io_helpers |
| Solidification model | solid | solidification_model |
Why good module names matter
- Readability: Clear names make it easier to navigate the codebase.
- Maintainability: Descriptive names reduce the need for excessive documentation.
- Onboarding: New developers can quickly understand the code structure.
- Collaboration: Teams can communicate more effectively about the code.
Indentation & whitespace
By setting and following a convention for indentation and whitespace, code reviews and git-diffs can focus on the semantics of the proposed changes rather than style and formatting.
- The body of every Fortran construct should be indented by three (3) spaces
- Line length should be limited to 132 characters and must not exceed it. This rule extends to in-line comments and comment lines
- Do not use Tab characters for indentation as they are an invalid Fortran character
- Remove trailing white space before committing code. VS Code can be configured to remove these when saving a file, or enforced with
.editorconfig - Whitespace should always be included in Fortran structure commands such as
do; end do,if; end if,intent(in out), etc. - Whitespace should always surround operators (
-, +, *, /, ==, <, >, etc), assignments (=), variable definitions (::), for clarity:var = x + y * zinstead ofvar=x+y*z. A notable exception might be the power operator**, which doesn't look good with spaces. However, because of formatter limitations it might be infeasible to isolate. - Whitespace should follow commas in comma delimited lists of arguments:
subroutine apply_boundaries(array, bc_types) real, intent(in) :: array(:, :, :) array(:, 1:ny, 1:nz) = [1, 2, 3, 4, 5] - Whitespace between methods, definitions, statements, control flow structures, or even regular code, should aim to be a single newline.
- Do not pile statements on a single line with
;. Break those up into different lines.
Continuation lines
Use the modern Fortran style for continuation lines, which is the & character following a comma:
call MPI_Sendrecv(buffer_send_y(1, 1), Y_FACE_SIZE, MPI_REAL, south, tag, &
buffer_rcv_y(1, 1), Y_FACE_SIZE, MPI_REAL, north, tag, &
comm_cart, status, ierr)
Indentation should align with the dummy argument list.
In named control flow blocks, continuation lines can be used to separate the name from the structure itself for expected indentation and alignment:
outer:&
do k = 1, nz
...
end do outer
Attributes
-
Always specify
intentfor dummy arguments,intent(in),intent(out), andintent(in out) -
Always initialize or assign an
intent(out)dummy variable -
Don't overuse
intent(in out)in places where either- variable is not being assigned -> only use
intent(in) - variable is not being read -> only use
intent(out). Locals can be used instead
- variable is not being assigned -> only use
-
Don't use
dimensionattribute to declare arrays because it is more verbose Use this:real, allocatable :: a(:), b(:,:)instead of:
real, dimension(:), allocatable :: a real, dimension(:,:), allocatable :: bWhen defining many arrays of the same dimension,
dimensioncan be used as an exception if it makes the code less verbose.However, first consider using
FORDcomments to break up the multiple definitions:!> 3D velocity components for the fluid flow solver real :: u(:,:,:), v(:,:,:), w(:,:,:) !> 3D pressure matrix for the fluid flow solver real :: pressure(:,:,:) !> 3D work array real :: work_array(:,:,:)instead of:
real, dimension(:,:,:) :: u, v, w, pressure, temp ! Fluid flow arrays -
If the
optionalattribute is used to declare a dummy argument, it should follow theintentattribute.
Scope block end closing statements
Always include the name of the scope block in the closing statement along with end. Scopes include program, module, subroutine, function, interface, and others.
Eg:
program foo
end program foo
! or
function foo()
end function foo
Comments
Styling
- Placement: Prefer comments on their own line, above the relevant code. Avoid inline comments unless absolutely necessary.
- Length: Limit comments to 132 characters per line.
- Formatting: Include a space after the comment delimiter (
!). For example:! Good !This is harder to read
Content
Comments should explain why the code exists, not what it does. If the code's intent isn't clear, consider refactoring it instead of adding a comment. Well-written code often speaks for itself.
- Explain Intent: Describe the purpose or reasoning behind the code.
- Avoid Redundancy: Don't state the obvious. For example:
! Bad: Describes what ! Increment i by 1 i = i + 1 ! Good: Explains why ! Adjust index to skip ghost cells i = i + num_ghost_layers - Tell a Story: Comments should help the code convey a clear, logical narrative.
When to Comment
- Complex Logic: Explain non-obvious algorithms or workarounds.
- Assumptions: Document any assumptions or constraints.
- TODOs: Use
! TODO:for temporary notes about future improvements.
Example
! Velocity magnitude is needed for turbulence modelling in the core domain
velocity_magnitude = sqrt(u**2 + v**2 + w**2)
Dead Code
Dead code is any part of the codebase that is no longer used or reachable. While it might be tempting to leave dead code "temporarily" (e.g., for future reference or debugging), it often becomes permanent, cluttering the codebase and obscuring the developer's intent.
Dead code should be eliminated whenever possible. It introduces unnecessary complexity, increases mental fatigue, and reduces readability and maintainability. A clean codebase is easier to understand, debug, and extend.
What Constitutes Dead Code?
Dead code includes, but is not limited to:
- Commented-out code: Blocks of code that are no longer in use but left in place.
- Unused variables: Variables that are declared but never referenced.
- Unused dummy arguments: Arguments in functions or subroutines that are never used.
- Unused functions or subroutines: Procedures that are defined but never called.
- Unreachable paths: Code that cannot logically be executed, such as:
if (.true. .eqv. .false.) call this_redundant_subroutine()
Why Remove Dead Code?
- Dead code distracts developers and makes it harder to focus on the active parts of the program.
- A clean codebase is easier to navigate and understand.
- Dead code can mislead developers into thinking it is still relevant or functional.
- Fewer lines of code mean fewer places for bugs to hide and less effort to maintain.
- While modern compilers often optimize away dead code, its presence can still affect compilation times and tooling efficiency.
How to Handle Dead Code
- Delete It: If the code is no longer needed, remove it entirely. Version control systems like Git preserve the history, so you can always recover it if necessary.
- Refactor: If the code is partially useful, refactor it into a cleaner, more maintainable form.
- Use Version Control: If you suspect the code might be needed later, rely on Git or your version control system to store it, rather than leaving it in the source files.
- Document Removal: If the removal is non-trivial, add a brief comment or commit message explaining why the code was removed.
Dead code is a liability, not an asset. By actively removing it, you keep the codebase clean, maintainable, and focused on its purpose. As the saying goes: "The best code is no code." When in doubt, delete it—your future self (and your team) will thank you.
Precision and Kinds
Fortran provides a variety of foot guns representations of real and integer numbers. You can read more about this subject in this blog post.
Do not use non-standard or non-portable precision arguments:
real*8 :: variable ! Non-standard
real(4) :: array(10) ! Non-portable
Instead, use the precision module to define and use numerical precision kinds for reals and integers.
module precision
use, intrinsic :: iso_fortran_env, only: real32, real64
integer, parameter :: sp = real32
integer, parameter :: dp = real64
Then, in your code:
module foo
use precision, only: sp, dp
real(sp) :: solid_fraction
real(dp) :: temperature
...
solid_fraction = 0.0_sp
temperature = 273.04_dp
Including kind= in the declaration is a matter of personal preference.
Kinds in Functions
For consistency, define your functions as:
function func() result(result)
integer :: result
instead of
integer function func()
Same with real and double precision functions.
Common pitfalls
-
Do not use the "implicit save" Fortran functionality during variable declarations:
! Bad - initializes with implicit SAVE attribute real :: counter = 0 ! Good - safe initialization real :: counter counter = 0Note that this is materially different from
parameters, which have to be assigned to during declaration.In cases where it's absolutely necessary, consider:
- refactoring: use module encapsulation
- thread-safety:
saved variables are non thread-safe
Enums
Enums (enumerations) are a powerful tool for managing states, categories, or discrete options in a program. They replace "magic numbers" (hard-coded integers) with meaningful names, improving code readability, maintainability, and reducing the risk of errors. While Fortran does not yet have native enum support (though it is coming soon™), the spirit of enums can be implemented using named constants.
Why use Enums?
Enums are particularly useful for:
- State Management: Representing states in a finite state machine (e.g.,
INITIALIZED,RUNNING,FINISHED). - Discrete Categories: Classifying entities (e.g.,
SOLID,LIQUID,GASfor material phases). - Directional or Positional Data: Representing directions (e.g.,
NORTH,SOUTH,EAST,WEST) or boundaries (e.g.,LEFT,RIGHT,TOP,BOTTOM). - Options or Modes: Configuring behavior (e.g.,
PERIODIC,DIRICHLET,NEUMANNfor boundary conditions).
By using enums, you make the code self-documenting and reduce the likelihood of errors caused by mixing up integer values.
Implementing Enums in Fortran
While Fortran lacks native enum support, you can achieve similar functionality using named constants:
!> Named constants for the 6 directions in a 3D domain.
integer, public, parameter :: D_WEST = 1, D_EAST = 2, D_SOUTH = 3, D_NORTH = 4, D_LOW = 5, D_HIGH = 6
!> Named constants for Boundary Conditions.
integer, public, parameter :: PERIODIC = 0, DIRICHLET = 1, NEUMANN = 2
This approach allows you to replace "magic numbers" with meaningful names:
select case (bc_types(face))
case (PERIODIC) ! Periodic is handled by MPI
cycle
case (DIRICHLET)
call apply_dirichlet(array, face, constant_value=dirichlet_value)
case (NEUMANN)
call apply_neumann(array, face)
...
subroutine apply_dirichlet(array, face, constant_value)
select case (face)
case (D_WEST)
array(1, :, :) = constant_value
case (D_EAST)
array(ubound(array, 1), :, :) = constant_value
MPI
Do not use the deprecated include 'mpif.h' header file. At this moment, the code should be using the old MPI Fortran module via use mpi. Replacing the former with the latter is a drop in replacement.
In the future, all code will be exclusively ported to mpi_f08, the more modern explicit Fortran interface.
- When modifying MPI code, convert
use mpicalls touse mpi_f08gradually, relying on compiler errors to amend and edit - Prefer MPI derived types (e.g.,
TYPE(MPI_Datatype)) for type safety
Document public API code with FORD
Documentation strings should be provided for all public and protected entities and their arguments or parameters. This is currently accomplished using the FORD tool.
Ford example:
!> Applies boundary conditions to each boundary face of the local domain,
!> only when the face belongs to the physical boundary.
subroutine apply_boundaries(array, bc_types)
use lib_parameters, only: dirichlet_value
!> Source array to update boundary conditions
real(kind=sp), contiguous, intent(in out) :: array(:, :, :)
integer, intent(in) :: bc_types(6)
!! The boundary conditions for each face/direction
integer :: face
!! Local loop variable - does not get included in documentation
...
subroutine determine_rank_boundaries()
!! Determines which faces of this rank are physical boundaries.
!!
!! Sets:
!! - `is_bc_face(face)` to TRUE when `face` has `MPI_PROC_NULL` neighbor
!! - `is_rank_inside` to TRUE when no `face` is a physical boundary
integer :: neighbour_ranks
! Local array of neighbor ranks for each face in 3D
neighbor_ranks = [west, east, south, north, low, high]
...
For help writing FORD style documentation please see the FORD wiki. The following two sections are most relevant for contributing new code:
Error Handling
Fatal Errors
For unrecoverable errors, use error stop with a descriptive message. This immediately terminates the program and provides context for debugging.
if (ierr /= 0) error stop "File read failed in module_io"
Recoverable Errors
For recoverable errors, use integer error codes or Enums to indicate the nature of the issue. These codes can be handled by the calling routine to attempt recovery or provide fallback behavior.
Example: Error codes in subroutines
subroutine check_convergence(operation, error)
integer, intent(out) :: error !< Error code (0 = success, non-zero = failure)
error = 0
if (.not. convergence) error = 1 ! Could also use an Enum for error codes
if (error == 1) return ! Early return on error
...
end subroutine check_convergence
Example: Handling errors in parent routines
call check_convergence(implicit_solver, error_code)
if (error_code /= 0) then
! Attempt recovery, e.g., by relaxing convergence criteria
call adjust_convergence_criteria()
call check_convergence(implicit_solver, error_code)
if (error_code /= 0) error stop "Failed to converge after adjustment"
end if
WIP
- Unit tests: Structure tests using a framework (e.g.,
pfUnit,FRUIT,Vegetables). - Performance:
- Prefer contiguous arrays; avoid unnecessary copies with
intent(in out). pureandelementalfunctions
- Prefer contiguous arrays; avoid unnecessary copies with
References
This document was based on the Fortran stdlib Style Guide and directly references the fortran-lang Best Practices.