Modern Fortran Development
This skill covers writing modern, maintainable Fortran (2003/2008 and later) for scientific and numerical computing, including module organization, kind parameters, procedure design, memory safety, and testing.
Workflow for Writing a Modern Fortran Module
- Define shared kinds first — Create a
kinds_mod (or similarly named) module with iso_fortran_env or selected_real_kind/selected_int_kind parameters used across the whole project.
- Design the module — Group related derived types and procedures into one focused module per file; declare
implicit none at the top.
- Write procedure interfaces — Give every dummy argument an explicit
intent(in), intent(out), or intent(inout); use use, only: to import exactly what's needed.
- Implement with early validation — Check preconditions (array bounds, allocation state, valid ranges) at the top of each procedure and return/stop early rather than nesting deeply.
- Manage arrays explicitly — Use allocatable arrays, check
allocated() before use, and deallocate when the lifetime isn't naturally scoped.
- Build with warnings on — Compile with
-Wall -Wextra -std=f2008 (gfortran) or the equivalent, and fail CI on new warnings.
- Test — Write unit tests for individual procedures and integration tests for full numerical workflows, checking tolerances rather than exact floating-point equality.
Basic Principles
- Target modern Fortran standards — Fortran 2003, 2008, or newer — and avoid writing in a legacy FORTRAN 77 style just because the compiler still accepts it.
- Put
implicit none in every program unit (module, program, and — via inheritance from a module or explicit statement — every procedure) so undeclared-variable typos are caught at compile time instead of producing silent wrong answers.
- Put procedures in modules rather than external subprograms; module procedures get automatically-generated explicit interfaces, which lets the compiler catch argument mismatches that external procedures cannot.
- Keep modules focused on one concern and place each major module in its own file, named to match the module (e.g.,
module linear_solver in linear_solver.f90).
- Prefer clear, structured code over clever language tricks — Fortran numerical code is read far more often than it's written, usually by someone other than the original author.
- Avoid obsolete features:
COMMON blocks (replace with modules), GOTO-heavy control flow (replace with structured if/do/select case, plus block where useful), and numeric statement labels used as jump targets.
Kinds and Types
- Define numeric kind parameters in one shared module (e.g.,
kind_mod or precision_mod) so the whole codebase can change precision in one place.
- Use
real(kind=dp) (or the project's approved real kind) for floating-point values — never bare real or double precision, whose actual precision is compiler- and flag-dependent.
- Use
integer(kind=i4) (or the project's approved integer kind) for integers where the width matters, especially in interfaces to C or binary I/O.
- Define constants such as
pi explicitly and precisely (e.g., real(dp), parameter :: pi = 4.0_dp * atan(1.0_dp)) rather than truncated literals.
- Include the physical units in a comment for any variable representing a physical quantity (
real(dp) :: velocity ! m/s).
- Use derived types to group related data (e.g., a
particle_t type with position, velocity, and mass fields) instead of passing many loose primitive arguments through procedure calls.
Example: Kinds Module and a Numerical Procedure
module kinds_mod
use iso_fortran_env, only: real64, int32
implicit none
private
public :: dp, i4
integer, parameter :: dp = real64
integer, parameter :: i4 = int32
end module kinds_mod
module stats_mod
use kinds_mod, only: dp
implicit none
private
public :: mean, standard_deviation
contains
pure function mean(x) result(m)
real(dp), intent(in) :: x(:)
real(dp) :: m
m = sum(x) / real(size(x), dp)
end function mean
pure function standard_deviation(x) result(s)
real(dp), intent(in) :: x(:)
real(dp) :: s
real(dp) :: m
integer :: n
n = size(x)
if (n < 2) then
s = 0.0_dp
return
end if
m = mean(x)
s = sqrt(sum((x - m)**2) / real(n - 1, dp))
end function standard_deviation
end module stats_mod
program demo
use kinds_mod, only: dp
use stats_mod, only: mean, standard_deviation
implicit none
real(dp) :: samples(5)
samples = [1.0_dp, 2.0_dp, 3.0_dp, 4.0_dp, 5.0_dp]
print '(A, F0.4)', 'mean = ', mean(samples)
print '(A, F0.4)', 'stddev = ', standard_deviation(samples)
end program demo
Naming and Style
- Use lowercase for language keywords and most identifiers; Fortran is case-insensitive, so consistent lowercase avoids visual noise from mixed-case keywords.
- Use underscores for multi-word names (
particle_velocity, not particleVelocity or ParticleVelocity).
- Avoid names that differ only by case (
Count vs count) since Fortran treats them as identical, inviting confusion.
- Use descriptive names for procedures and state (
compute_residual, not cr or calc2).
- Repeat the procedure or module name after
end statements (end module kinds_mod, end subroutine compute_residual) so long units are easy to verify by eye.
- Keep indentation consistent (2 or 4 spaces, matching the project) inside
do, if, select case, and module blocks.
Procedures
- Keep subroutines and functions short and single-purpose — a procedure that computes a residual should not also write output files.
- Use
intent(in), intent(out), or intent(inout) for every dummy argument; an argument with no intent is a red flag that the interface hasn't been thought through.
- Keep functions free of side effects whenever possible (mark them
pure or elemental when they qualify) and reserve subroutines for procedures that mutate state or perform I/O.
- Prefer early validation and clear returns (
if (n <= 0) then; ...; return; end if) over deeply nested if blocks.
- Use
use, only: name1, name2 when importing from a module instead of a bare use module_name, so it's clear at the call site exactly what's being pulled in and name collisions are avoided.
Memory and Arrays
- Prefer allocatable arrays over pointers unless pointer semantics (aliasing, linked structures) are specifically required — allocatables are automatically deallocated and safer by default.
- Check
allocated() state and array sizes (size(), lbound(), ubound()) before using an array that might not have been allocated yet.
- Deallocate allocatable arrays explicitly when their lifetime isn't naturally scoped (e.g., held in a derived type that outlives a single procedure call); arrays local to a procedure are deallocated automatically on exit.
- Specify array bounds clearly when they matter, especially non-default lower bounds (
real(dp) :: a(0:n)).
- Avoid unnecessary dynamic allocation inside hot loops — allocate once outside the loop and reuse the buffer, or use automatic/stack arrays for small, fixed-size temporaries.
Testing and Build
- Use CMake, fpm (the Fortran Package Manager), Make, or whatever build system the project has standardized on — consistently, not a mix.
- Compile with warnings enabled (
gfortran -Wall -Wextra -std=f2008 -fcheck=all for development builds) and treat important warnings as CI failures.
- Add unit tests for public procedures (a pure function like
standard_deviation should have a small, fast test with known input/output) and integration tests for full numerical workflows.
- Test boundary conditions (empty arrays, single-element arrays, zero/negative inputs), invalid inputs, and representative scientific cases drawn from the actual problem domain.
- Verify numerical results against a tolerance (e.g.,
abs(actual - expected) < 1.0e-10_dp) rather than relying on exact floating-point equality, which is almost never guaranteed across compilers or optimization levels.
Common Mistakes
- Declaring variables after executable statements without wrapping them in a
block construct — Fortran requires all declarations before executable code in a given scoping unit.
- Assuming
random_number is a function; it is a subroutine (call random_number(x), not x = random_number()).
- Writing to stdout (
print, write(*,*)) from a procedure declared pure — this is not allowed and will fail to compile.
- Declaring the same variable twice in the same scope, which is easy to miss when a module has grown large.
- Assuming
pi, dp, or other project-standard kind/constant names already exist without importing or defining them explicitly via use.
1---2name: fortran3description: Best practices for modern Fortran (2003/2008+) scientific and numerical computing, covering modules, explicit interfaces, kind parameters, memory safety, and testing. Use when writing or reviewing Fortran source (.f90/.f95/.f03/.f08), defining modules and derived types, choosing numeric kind parameters, working with allocatable arrays, setting up a Fortran build with CMake or fpm, or writing unit tests for numerical code.4---5
6# Modern Fortran Development
7
8This skill covers writing modern, maintainable Fortran (2003/2008 and later) for scientific and numerical computing, including module organization, kind parameters, procedure design, memory safety, and testing.
9
10## Workflow for Writing a Modern Fortran Module
11
121. **Define shared kinds first** — Create a `kinds_mod` (or similarly named) module with `iso_fortran_env` or `selected_real_kind`/`selected_int_kind` parameters used across the whole project.
132. **Design the module** — Group related derived types and procedures into one focused module per file; declare `implicit none` at the top.
143. **Write procedure interfaces** — Give every dummy argument an explicit `intent(in)`, `intent(out)`, or `intent(inout)`; use `use, only:` to import exactly what's needed.
154. **Implement with early validation** — Check preconditions (array bounds, allocation state, valid ranges) at the top of each procedure and return/stop early rather than nesting deeply.
165. **Manage arrays explicitly** — Use allocatable arrays, check `allocated()` before use, and deallocate when the lifetime isn't naturally scoped.
176. **Build with warnings on** — Compile with `-Wall -Wextra -std=f2008` (gfortran) or the equivalent, and fail CI on new warnings.
187. **Test** — Write unit tests for individual procedures and integration tests for full numerical workflows, checking tolerances rather than exact floating-point equality.
19
20## Basic Principles
21
22- Target modern Fortran standards — Fortran 2003, 2008, or newer — and avoid writing in a legacy FORTRAN 77 style just because the compiler still accepts it.
23- Put `implicit none` in every program unit (module, program, and — via inheritance from a module or explicit statement — every procedure) so undeclared-variable typos are caught at compile time instead of producing silent wrong answers.
24- Put procedures in modules rather than external subprograms; module procedures get automatically-generated explicit interfaces, which lets the compiler catch argument mismatches that external procedures cannot.
25- Keep modules focused on one concern and place each major module in its own file, named to match the module (e.g., `module linear_solver` in `linear_solver.f90`).
26- Prefer clear, structured code over clever language tricks — Fortran numerical code is read far more often than it's written, usually by someone other than the original author.
27- Avoid obsolete features: `COMMON` blocks (replace with modules), `GOTO`-heavy control flow (replace with structured `if`/`do`/`select case`, plus `block` where useful), and numeric statement labels used as jump targets.
28
29## Kinds and Types
30
31- Define numeric kind parameters in one shared module (e.g., `kind_mod` or `precision_mod`) so the whole codebase can change precision in one place.
32- Use `real(kind=dp)` (or the project's approved real kind) for floating-point values — never bare `real` or `double precision`, whose actual precision is compiler- and flag-dependent.
33- Use `integer(kind=i4)` (or the project's approved integer kind) for integers where the width matters, especially in interfaces to C or binary I/O.
34- Define constants such as `pi` explicitly and precisely (e.g., `real(dp), parameter :: pi = 4.0_dp * atan(1.0_dp)`) rather than truncated literals.
35- Include the physical units in a comment for any variable representing a physical quantity (`real(dp) :: velocity ! m/s`).
36- Use derived types to group related data (e.g., a `particle_t` type with position, velocity, and mass fields) instead of passing many loose primitive arguments through procedure calls.
37
38### Example: Kinds Module and a Numerical Procedure
39
40```fortran
41module kinds_mod
42 use iso_fortran_env, only: real64, int32
43 implicit none
44 private
45 public :: dp, i4
46
47 integer, parameter :: dp = real64
48 integer, parameter :: i4 = int32
49end module kinds_mod
50```
51
52```fortran
53module stats_mod
54 use kinds_mod, only: dp
55 implicit none
56 private
57 public :: mean, standard_deviation
58
59contains
60
61 pure function mean(x) result(m)
62 real(dp), intent(in) :: x(:)
63 real(dp) :: m
64
65 m = sum(x) / real(size(x), dp)
66 end function mean
67
68 pure function standard_deviation(x) result(s)
69 real(dp), intent(in) :: x(:)
70 real(dp) :: s
71 real(dp) :: m
72 integer :: n
73
74 n = size(x)
75 if (n < 2) then
76 s = 0.0_dp
77 return
78 end if
79
80 m = mean(x)
81 s = sqrt(sum((x - m)**2) / real(n - 1, dp))
82 end function standard_deviation
83
84end module stats_mod
85```
86
87```fortran
88program demo
89 use kinds_mod, only: dp
90 use stats_mod, only: mean, standard_deviation
91 implicit none
92
93 real(dp) :: samples(5)
94 samples = [1.0_dp, 2.0_dp, 3.0_dp, 4.0_dp, 5.0_dp]
95
96 print '(A, F0.4)', 'mean = ', mean(samples)
97 print '(A, F0.4)', 'stddev = ', standard_deviation(samples)
98end program demo
99```
100
101## Naming and Style
102
103- Use lowercase for language keywords and most identifiers; Fortran is case-insensitive, so consistent lowercase avoids visual noise from mixed-case keywords.
104- Use underscores for multi-word names (`particle_velocity`, not `particleVelocity` or `ParticleVelocity`).
105- Avoid names that differ only by case (`Count` vs `count`) since Fortran treats them as identical, inviting confusion.
106- Use descriptive names for procedures and state (`compute_residual`, not `cr` or `calc2`).
107- Repeat the procedure or module name after `end` statements (`end module kinds_mod`, `end subroutine compute_residual`) so long units are easy to verify by eye.
108- Keep indentation consistent (2 or 4 spaces, matching the project) inside `do`, `if`, `select case`, and module blocks.
109
110## Procedures
111
112- Keep subroutines and functions short and single-purpose — a procedure that computes a residual should not also write output files.
113- Use `intent(in)`, `intent(out)`, or `intent(inout)` for every dummy argument; an argument with no `intent` is a red flag that the interface hasn't been thought through.
114- Keep functions free of side effects whenever possible (mark them `pure` or `elemental` when they qualify) and reserve `subroutine`s for procedures that mutate state or perform I/O.
115- Prefer early validation and clear returns (`if (n <= 0) then; ...; return; end if`) over deeply nested `if` blocks.
116- Use `use, only: name1, name2` when importing from a module instead of a bare `use module_name`, so it's clear at the call site exactly what's being pulled in and name collisions are avoided.
117
118## Memory and Arrays
119
120- Prefer allocatable arrays over pointers unless pointer semantics (aliasing, linked structures) are specifically required — allocatables are automatically deallocated and safer by default.
121- Check `allocated()` state and array sizes (`size()`, `lbound()`, `ubound()`) before using an array that might not have been allocated yet.
122- Deallocate allocatable arrays explicitly when their lifetime isn't naturally scoped (e.g., held in a derived type that outlives a single procedure call); arrays local to a procedure are deallocated automatically on exit.
123- Specify array bounds clearly when they matter, especially non-default lower bounds (`real(dp) :: a(0:n)`).
124- Avoid unnecessary dynamic allocation inside hot loops — allocate once outside the loop and reuse the buffer, or use automatic/stack arrays for small, fixed-size temporaries.
125
126## Testing and Build
127
128- Use CMake, fpm (the Fortran Package Manager), Make, or whatever build system the project has standardized on — consistently, not a mix.
129- Compile with warnings enabled (`gfortran -Wall -Wextra -std=f2008 -fcheck=all` for development builds) and treat important warnings as CI failures.
130- Add unit tests for public procedures (a pure function like `standard_deviation` should have a small, fast test with known input/output) and integration tests for full numerical workflows.
131- Test boundary conditions (empty arrays, single-element arrays, zero/negative inputs), invalid inputs, and representative scientific cases drawn from the actual problem domain.
132- Verify numerical results against a tolerance (e.g., `abs(actual - expected) < 1.0e-10_dp`) rather than relying on exact floating-point equality, which is almost never guaranteed across compilers or optimization levels.
133
134## Common Mistakes
135
136- Declaring variables after executable statements without wrapping them in a `block` construct — Fortran requires all declarations before executable code in a given scoping unit.
137- Assuming `random_number` is a function; it is a subroutine (`call random_number(x)`, not `x = random_number()`).
138- Writing to stdout (`print`, `write(*,*)`) from a procedure declared `pure` — this is not allowed and will fail to compile.
139- Declaring the same variable twice in the same scope, which is easy to miss when a module has grown large.
140- Assuming `pi`, `dp`, or other project-standard kind/constant names already exist without importing or defining them explicitly via `use`.