Nml-tools v0.3: schema-driven Fortran namelist helpers

I would like to share a short update on nml-tools, a small Python tool that generates Fortran namelist modules from YAML/JSON schema-like files.

The idea is to describe each namelist once and generate the repetitive pieces from that description: Fortran declarations, init / set / from_file helpers, validation, Markdown documentation, template namelist files, and optionally f2py/Python wrapper code.

It is available on PyPI:

pip install nml-tools

Basic Example

A schema describes the namelist block and its entries:

title: Optimization settings
x-fortran-namelist: optimization
type: object
required: [max_iter]
properties:
  max_iter:
    type: integer
    x-fortran-kind: i4
    default: 100
  method:
    type: string
    x-fortran-len: 16
    enum: [bfgs, nelder_mead]
    default: bfgs

With a small project config, nml-tools generate --config nml-config.toml can emit the Fortran module, documentation, and a template namelist.

The config can be a standalone nml-config.toml file or a [tool.nml-tools] section inside pyproject.toml, which is convenient for Python/f2py packages.

Reusable Schema Definitions

Version 0.3 adds $defs / $ref support, so common field definitions can be shared between namelists:

$defs:
  positive_count:
    type: integer
    minimum: 1
    x-fortran-kind: i4

properties:
  n_steps:
    $ref: "#/$defs/positive_count"
    description: Number of time steps.

References can also point to local YAML/JSON files next to the schema, which is useful for larger applications with several namelists.

One-Level Derived Types

nml-tools can now model one level of Fortran derived-type fields. A type can be generated into the helper module or imported from an application module:

properties:
  period:
    type: object
    x-fortran-type: period_t
    required: [start_year, end_year]
    properties:
      start_year:
        type: integer
      end_year:
        type: integer

Generated namelist input uses normal Fortran component syntax:

&run
  period%start_year = 2001
  period%end_year = 2010
/

For now this is deliberately conservative: derived-type members must be scalar intrinsic values, and nested derived types or array members are not supported.

f2py/Python Wrappers

For projects that expose Fortran configuration to Python, v0.3 can generate f2py-facing wrapper code and a small Python shim. The Python API keeps derived values natural:

cfg.set(
    period={"start_year": 2001, "end_year": 2010},
    method="bfgs",
)

Internally the f2py ABI stays intrinsic-only, avoiding derived-type dummy arguments across the Python/Fortran boundary. The generated wrapper works with Fortran-owned configuration objects through opaque integer handles, so Python does not need to know the layout of the generated Fortran type.

File Profiles And Templates

There is also better support for projects where one physical namelist file contains several namelist blocks:

[[namelists]]
name = "run"
schema = "schemas/run.yml"

[[namelists]]
name = "physics"
schema = "schemas/physics.yml"

[[file_profiles]]
name = "main"
default_file = "run.nml"
namelists = ["run", "physics"]

[[templates]]
path = "templates/run.nml"
profile = "main"
doc_mode = "documented"
value_mode = "filled"

This lets generated templates and validation operate on the same named file layout.

The repository contains small reference examples for the main workflows, including plain Fortran generation, f2py packaging, $ref reuse, derived types, and multi-namelist file profiles.

Feedback from Fortran users would be very welcome, especially around the schema syntax, derived-type restrictions, and what a convenient namelist editing workflow should look like.

3 Likes

this is great, I recently discovered namelist files and instead of reinventing the wheel to read weird input files in Fortran I’ve done something similar to this. I use python to read something like json or yaml and translate it.

Does your package have a schema enforcing option? i.e. fail if it detects a key that is not allowed?

This is great! I wish I knew about this earlier. I had been developing my own tool, geninput, to simultaneously generate namelist boilerplate code and LaTeX documentation. nml-tools is a lot more polished than geninput and has more features. One feature geninput has that I didn’t see mentioned in the README is the ability to specify the unit of the key, which I suggest adding.

The earliest input code generation tool I’ve seen for Fortran was discussed in the book Writing good software in FORTRAN by Graham Smith back in 1988. But Smith’s system doesn’t generate both code and documentation.

Thank you!

Yes, this error is catched during reading (since the namelist read fails by the Fortran standard) and a NML_ERR_READ error code is returned with the compiler dependent io-error message from the namelist read.

The part I was missing in namelist reading was the opposite: non present variables can’t be detected in a canonical way. That was the main motivation for me to develop nml-tools.
This is done by setting type specific sentinels and checking for them after the namelist read.

And this helps with the schema enforcing: required variables need to overwrite the sentinel and simple restrictions like min/max/enum-values are also implemented (following json-schema).

Thank you! Since units are only meta-data to me (integration into a unit-handling framework is out of scope for nml-tools), I would not create a mechanism for unit handling (also, since I want to stay close to json-schema).

From my POV, units should be documented in the description, or maybe more prominently in the title of a variable.

1 Like

Another suggestion would be to make one of the example generated Markdown documentation files more prominent. The advantage of generating consistent code and documentation will become a lot more clear to people if they look at this example already in the repo, for instance. (Here’s an example from my own system, geninput. It’s fairly different. I should add the key bounds to the generated documentation now that I’m comparing against nml-tools.)

I’ve maintained consistent namelist processing code including input validation and LaTeX documentation before, and it can be a pain. The documentation is often out of sync with the code or even incorrect. But in this case, the two are perfectly in sync. In my geninput system, I think at some point I found a bug in the code from reading the documentation, where if the two were not synchronized then the documentation likely would have reflected what was intended, hiding the bug.

Agree. Currently the whole package documentation is in the readme. A dedicated RTD page would be a good idea for the future with in-depth discussion of features.