ZetCode

Fortran derived types

last modified November 14, 2026

Parallel arrays, one for the names and one for the scores, work until there are five columns and a function has to remember which index belongs to which. A derived type is Fortran's way to define a new type by grouping named variables, called components, into one value. A variable of that type then travels as a single item, can be copied with one assignment, and can be passed to a procedure without listing its parts. This article defines types with components and defaults, builds values with the structure constructor, nests one type inside another, keeps arrays of them, passes them to functions, and gives a type arrays and strings whose size is decided at run time.

Defining a type

A type definition sits between type :: name and end type, and each component is declared inside it like an ordinary variable. Variables of the type are then declared with type(name) :: variable, and a component is reached with the percent sign, as in p%x. The structure constructor point(1.0, 2.5) builds a value from the components in the order in which they were declared, which is the shortest way to fill a whole variable.

d1.f90
program d1
    implicit none
    type :: point
        real :: x, y
    end type point
    type(point) :: p, q

    p = point(1.0, 2.5)
    q%x = -3.0
    q%y = 0.5
    print *, 'p:', p
    print *, 'q:', q
    print *, 'p%x =', p%x, ' p%y =', p%y
end program d1

The constructor fills both components of p at once, while q is filled one component at a time. Printing a whole derived type is allowed when the type is simple, and the runtime writes the components in declaration order, separated by blanks, which is why p: is followed by the two numbers. The type definition is local to the program here; a type that several procedures share lives in a module, as the examples with functions below show.

$ gfortran d1.f90 -o d1
$ ./d1
 p:   1.00000000       2.50000000    
 q:  -3.00000000      0.500000000    
 p%x =   1.00000000      p%y =   2.50000000    

Default values

A component may be given a default value in its declaration, and that value is used whenever a variable of the type is created without one. Defaults turn a type into a safe starting point: a fresh variable already has sensible contents instead of undefined bits. The constructor can still override every default, and a component can be changed later like any other variable. The program below shows a type with four defaults, a fully specified constructor, and one component changed after construction.

d2.f90
program d2
    implicit none
    type :: particle
        character(len=10) :: name = 'unknown'
        real :: mass = 1.0
        real :: charge = 0.0
        logical :: stable = .true.
    end type particle
    type(particle) :: a, b

    print *, 'default: ', a
    b = particle('electron', 9.11e-31, -1.0, .true.)
    print *, 'b:       ', b%name, b%mass, b%charge, b%stable
    b%stable = .false.
    print *, 'changed: ', b%stable
end program d2

The default variable prints as unknown, a mass of one, a charge of zero and a stable flag of true, so no component was left undefined. The constructor then replaces all four, and the assignment to b%stable changes one component of an existing value. The name prints with trailing blanks because it was declared with ten characters; the logical value prints as T or F in list-directed output.

$ gfortran d2.f90 -o d2
$ ./d2
 default: unknown      1.00000000       0.00000000     T
 b:       electron     9.11000034E-31  -1.00000000     T
 changed:  F

Nested types

A component can itself have a derived type, which is how a rectangle is built from two corners. The constructor of the outer type then takes constructor calls as arguments, as in rectangle(point(1.0, 2.0), point(4.0, 6.0), 'window'), and the components of the inner value are reached with a second percent sign: r%hi%x is the x coordinate of the upper corner. This nesting can go as deep as the problem needs.

d3.f90
program d3
    implicit none
    type :: point
        real :: x = 0.0, y = 0.0
    end type point
    type :: rectangle
        type(point) :: lo, hi
        character(len=8) :: label = 'box'
    end type rectangle
    type(rectangle) :: r
    real :: area

    r = rectangle(point(1.0, 2.0), point(4.0, 6.0), 'window')
    area = (r%hi%x - r%lo%x) * (r%hi%y - r%lo%y)
    print *, r%label
    print *, 'corner:', r%lo%x, r%lo%y, 'to', r%hi%x, r%hi%y
    print *, 'area:  ', area
end program d3

The lower corner is (1, 2) and the upper one is (4, 6), so the sides are three and four and the area is twelve. The label prints with its padding, which is why the line ends after a few blanks. Building the rectangle with the constructor assigns both nested point values in one statement, and the arithmetic reads the coordinates directly through the chain of percent signs.

$ gfortran d3.f90 -o d3
$ ./d3
 window  
 corner:   1.00000000       2.00000000     to   4.00000000       6.00000000    
 area:     12.0000000    

Arrays of a derived type

An array of a derived type is declared like any other array, and a constructor fills it with one value per element. Whole-array operations work on the array itself, but a component needs the percent sign after the array name: s%score is an array of integers, so sum(s%score) adds every score without a loop. Swapping two elements moves all their components together, which is the reason to group them in the first place.

d4.f90
program d4
    implicit none
    type :: student
        character(len=12) :: name
        integer :: score
    end type student
    type(student) :: s(4), tmp
    integer :: i, j

    s = [student('alice', 91), student('bob', 78), &
         student('carol', 84), student('dave', 67)]
    do i = 1, 3
        do j = i + 1, 4
            if (s(j)%score > s(i)%score) then
                tmp = s(i)
                s(i) = s(j)
                s(j) = tmp
            end if
        end do
    end do
    do i = 1, 4
        print *, i, s(i)%name, s(i)%score
    end do
    print *, 'average:', sum(s%score) / real(size(s))
end program d4

The sort puts the scores in descending order and carries the names with them, so alice and her 91 stay together. The temporary tmp is a whole student, not a name and a number, which makes the swap three assignments instead of five. The average is computed from the component array and converted to real before the division, because dividing an integer sum by an integer size would truncate the result.

$ gfortran d4.f90 -o d4
$ ./d4
           1 alice                 91
           2 carol                 84
           3 bob                   78
           4 dave                  67
 average:   80.0000000    

Types in procedures

A type defined in a module is visible to every procedure that uses the module, so it can appear in a procedure's argument list. A dummy argument declared type(point), intent(in) promises not to change the value, while intent(inout) allows the procedure to modify it. The functions below also return a value of the type, which is how a midpoint is naturally expressed.

d5.f90
module geom
    implicit none
    type, public :: point
        real :: x = 0.0, y = 0.0
    end type point
contains
    pure function distance(a, b) result(d)
        type(point), intent(in) :: a, b
        real :: d

        d = sqrt((b%x - a%x)**2 + (b%y - a%y)**2)
    end function distance

    pure function midpoint(a, b) result(m)
        type(point), intent(in) :: a, b
        type(point) :: m

        m = point((a%x + b%x) / 2.0, (a%y + b%y) / 2.0)
    end function midpoint

    subroutine shift(p, dx, dy)
        type(point), intent(inout) :: p
        real, intent(in) :: dx, dy

        p%x = p%x + dx
        p%y = p%y + dy
    end subroutine shift
end module geom

program d5
    use geom
    implicit none
    type(point) :: a, b, m

    a = point(0.0, 0.0)
    b = point(3.0, 4.0)
    print *, 'distance:', distance(a, b)
    m = midpoint(a, b)
    print *, 'midpoint:', m%x, m%y
    call shift(b, -1.0, 1.0)
    print *, 'shifted: ', b%x, b%y
    print *, 'new dist:', distance(a, b)
end program d5

The distance from the origin to (3, 4) is 5, the midpoint is (1.5, 2), and shifting the second point to (2, 5) changes the distance to about 5.385. The module gives every procedure an explicit interface, so the compiler checks the number and the types of the arguments, including the derived type. shift works in place because its argument is intent(inout); the two functions leave their arguments alone and return new values.

$ gfortran d5.f90 -o d5
$ ./d5
 distance:   5.00000000    
 midpoint:   1.50000000       2.00000000    
 shifted:    2.00000000       5.00000000    
 new dist:   5.38516474    

Allocatable components

A polygon has a number of vertices that is not known when the type is defined, so its coordinate arrays are declared with allocatable. Assigning an array constructor to tri%x allocates it automatically with the right size, and allocated() reports whether a component currently holds storage. The arrays are deallocated with the type when the variable goes out of scope, but a program that replaces them many times can free them explicitly to keep the memory use predictable.

d6.f90
program d6
    implicit none
    type :: polygon
        character(len=12) :: name = 'polygon'
        real, allocatable :: x(:), y(:)
    end type polygon
    type(polygon) :: tri
    real :: perimeter
    integer :: i, n

    tri%name = 'triangle'
    tri%x = [0.0, 4.0, 0.0]
    tri%y = [0.0, 0.0, 3.0]
    n = size(tri%x)
    print *, tri%name, 'has', n, 'vertices'
    print *, 'x:', tri%x
    print *, 'y:', tri%y

    perimeter = 0.0
    do i = 1, n - 1
        perimeter = perimeter + &
            sqrt((tri%x(i + 1) - tri%x(i))**2 + (tri%y(i + 1) - tri%y(i))**2)
    end do
    perimeter = perimeter + sqrt((tri%x(1) - tri%x(n))**2 + (tri%y(1) - tri%y(n))**2)
    print *, 'perimeter:', perimeter
    print *, 'allocated: ', allocated(tri%x)
    deallocate (tri%x, tri%y)
    print *, 'allocated: ', allocated(tri%x)
end program d6

The triangle has three vertices, and its sides are four, three and five, so the perimeter is twelve. The loop adds the distance from each vertex to the next one and stops at n - 1, because the last segment back to the first vertex would otherwise read past the end of the array; that closing segment is added after the loop. The two allocated lines show the component holding storage and then releasing it, which is the same lifecycle as a standalone allocatable array.

$ gfortran d6.f90 -o d6
$ ./d6
 triangle    has           3 vertices
 x:   0.00000000       4.00000000       0.00000000    
 y:   0.00000000       0.00000000       3.00000000    
 perimeter:   12.0000000    
 allocated:  T
 allocated:  F

Deferred-length components

A character component can also have a length chosen at run time, with character(len=:), allocatable. The component then stores exactly the characters assigned to it, with no padding, which is the right choice for names read from a file. One restriction matters here: a list-directed read cannot target an unallocated deferred-length component, so the program reads into an ordinary fixed-length variable first and then assigns the trimmed text to the component.

d7.f90
program d7
    implicit none
    type :: person
        character(len=:), allocatable :: name
        integer :: year
    end type person
    type(person), allocatable :: people(:)
    type(person) :: p
    character(len=80) :: line
    character(len=30) :: name
    integer :: u, ios, n

    open (newunit=u, file='people.txt', status='replace')
    write (u, '(a)') 'Ada 1815'
    write (u, '(a)') 'Grace 1906'
    write (u, '(a)') 'John 1924'
    close (u)

    allocate (people(0))
    open (newunit=u, file='people.txt', status='old')
    do
        read (u, '(a)', iostat=ios) line
        if (ios /= 0) exit
        read (line, *, iostat=ios) name, p%year
        if (ios /= 0) cycle
        p%name = trim(name)
        people = [people, p]
    end do
    close (u)

    print *, 'records:', size(people)
    do n = 1, size(people)
        print *, people(n)%year, people(n)%name
    end do
    print *, 'lengths:', [(len(people(n)%name), n = 1, size(people))]
end program d7

Three records are read, and the lengths of the stored names are 3, 5 and 4, which shows that each component was allocated with exactly the number of letters assigned to it. The array of people grows by one element per record, and each new element carries its own allocatable component, so the strings are copied along with the rest of the value. Appending many records this way copies the whole array each time; a move_alloc or a two-pass count avoids that for large files.

$ gfortran d7.f90 -o d7
$ ./d7
 records:           3
        1815 Ada
        1906 Grace
        1924 John
 lengths:           3           5           4

Next steps

A type becomes much more powerful when its procedures are declared inside it: the type-bound procedures article gives a type its own methods, the inheritance and polymorphism article extends one type from another and selects among the variants at run time, and the operator overloading article teaches a type to use + and = like a built-in value. The arrays article explains the constructors used to fill arrays of types, and the CSV and text data article replaces parallel arrays with the records defined here.

Source

Fortran 2018 standard (J3/18-007r1)

GNU Fortran manual

Fortran-lang: learning resources

In this article we have defined types with components and defaults, built values with structure constructors, nested one type in another, kept arrays of records, passed types to functions and subroutines, and given a type allocatable arrays and deferred-length strings. The previous article, CSV and text data in Fortran, uses parallel arrays for the same job and shows why grouping the columns is the next step.

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Fortran tutorials.