Repository: ecmwf/earthkit-data
Pull request #522 (2024-11-14 15:04:00)

Feature/review copy and to_field
The `copy()` and the `to_field()` methods were added to the current `develop` branch. This PR is dedicated to reviewing them before version `0.11.0` is released. To sum it up the suggestion is roughly to use `clone()` instead of `copy()` and `copy()` instead of `to_field()` (see details below).

The following notebook demonstrates the implementation using `clone()` and `copy()`:

https://earthkit-data--522.org.readthedocs.build/en/522/examples/grib_modification.html

The proposed behaviour
---------------------------

(Result of internal discussion).

Terminology: 
- "Cloned field": this is the object the proposed `clone()` generates. For each field type it is a different type (e.g. ClonedGribField). It has the same methods as the original field and keeps a reference to the original field. Created with inheritance from the original field type.
 
  - Values:
     - If no values set in `clone()` the values are accessed from the original field
     - Otherwise it uses the values set by `clone()`
      
  - Metadata:
      - If no metadata set in `clone()` the metadata from the original field is used
      - If a metadata object is set it will be used in the new field
      - Otherwise it will store a WrappedMetadata object, which wraps the original metadata object in a
         way that the keys not set in `clone()` are served from the original metadata, while the new keys 
         served from a separately stored dict.

- ArrayField: contains a values array and a (standalone) metadata object
 
The methods: 
 
clone
------
 
Always creates a cloned field.  The metadata, when specified, must be a dict/**kwargs.
 
`new_f = f.clone()`
Creates a cloned field, using evrything from the original field
 
`new_f = f.clone(values=new_values)`
Creates a cloned field with new values
 
`new_f = f.clone(level=700)`
Creates a cloned field with wrapped metadata
 
`new_f = f.clone(values=new_values, level=700)`
Creates a cloned field with new values and wrapped metadata
 
`new_f = f.clone(metadata=f.metadata().override(level=700))`
Creates a cloned field with the specified metadata object

`new_f = f.clone(values=new_values, metadata=f.metadata().override(level=700))`
 Creates a cloned field with the specified values and metadata object

copy
-------
Always creates an ArrayField. The `metadata`, when specified must be a Metadata object.
 
`new_f = f.copy()`
Creates an ArrayField by copying the values and the metadata (the latter with `metadata().override()`)
 
`new_f = f.copy(values=new_values)`
Creates an ArrayField with new values and copied metadata (the latter with `metadata().override()`)
 
`new_f = f.copy(metadata=f.metadata().override(level=700))`
Creates an ArrayField with values copied from the original field and new metadata
 
`new_f = f.copy(values=new_values, metadata=f.metadata().override(level=700))`
Creates an ArrayField with new values and metadata

codecov-commenter: ## [Codecov](https://app.codecov.io/gh/ecmwf/earthkit-data/pull/522?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ecmwf) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 91.08%. Comparing base [(`7efe4da`)](https://app.codecov.io/gh/ecmwf/earthkit-data/commit/7efe4da924df4aa632c914330e95967579bf409d?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ecmwf) to head [(`c8f73bd`)](https://app.codecov.io/gh/ecmwf/earthkit-data/commit/c8f73bde870314203ea2e4d65ed02d5c3f91b308?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ecmwf).

<details><summary>Additional details and impacted files</summary>


```diff
@@           Coverage Diff            @@
##           develop     #522   +/-   ##
========================================
  Coverage    91.08%   91.08%           
========================================
  Files          147      147           
  Lines        10730    10732    +2     
  Branches       510      510           
========================================
+ Hits          9773     9775    +2     
  Misses         794      794           
  Partials       163      163           
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/ecmwf/earthkit-data/pull/522?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ecmwf).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ecmwf).


Patch:
diff --git a/docs/examples/grib_modification.ipynb b/docs/examples/grib_modification.ipynb
new file mode 100644
index 0000000..50e1b1a
--- /dev/null
+++ b/docs/examples/grib_modification.ipynb
@@ -0,0 +1,1096 @@
+{
+ "cells": [
+  {
+   "cell_type": "markdown",
+   "id": "6b0660f7-ca38-420e-b6db-64326c0b5266",
+   "metadata": {
+    "editable": true,
+    "slideshow": {
+     "slide_type": ""
+    },
+    "tags": []
+   },
+   "source": [
+    "## GRIB: modifying fields"
+   ]
+  },
+  {
+   "cell_type": "raw",
+   "id": "f65ece3d-feaa-4ea7-a1cb-ca0eef8056d2",
+   "metadata": {
+    "editable": true,
+    "raw_mimetype": "text/restructuredtext",
+    "slideshow": {
+     "slide_type": ""
+    },
+    "tags": []
+   },
+   "source": [
+    "This notebook explains how to use :py:meth:`~data.core.fieldlist.Field.clone` and :py:meth:`~data.core.fieldlist.Field.copy` to create new fields. The main difference between these methods can be summarised as follows:\n",
+    "\n",
+    "- :py:meth:`~data.core.fieldlist.Field.clone`: the new field is a shallow copy. Allows more flexible metadata modification than using :py:meth:`~data.core.fieldlist.Field.copy`\n",
+    "- :py:meth:`~data.core.fieldlist.Field.copy`: always creates a deep copy. The resulting field is always an :py:class:`~data.sources.array_list.ArrayField` storing the all the data in memory"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "5b501f0b-1b9b-4c03-9cf5-7bf475d60916",
+   "metadata": {
+    "editable": true,
+    "slideshow": {
+     "slide_type": ""
+    },
+    "tags": []
+   },
+   "source": [
+    "We will use the first field of the input data in the examples."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 1,
+   "id": "2f3fb883-020f-4050-9026-0a3d5c553899",
+   "metadata": {
+    "editable": true,
+    "slideshow": {
+     "slide_type": ""
+    },
+    "tags": []
+   },
+   "outputs": [
+    {
+     "data": {
+      "text/html": [
+       "<div>\n",
+       "<style scoped>\n",
+       "    .dataframe tbody tr th:only-of-type {\n",
+       "        vertical-align: middle;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe tbody tr th {\n",
+       "        vertical-align: top;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe thead th {\n",
+       "        text-align: right;\n",
+       "    }\n",
+       "</style>\n",
+       "<table border=\"1\" class=\"dataframe\">\n",
+       "  <thead>\n",
+       "    <tr style=\"text-align: right;\">\n",
+       "      <th></th>\n",
+       "      <th>centre</th>\n",
+       "      <th>shortName</th>\n",
+       "      <th>typeOfLevel</th>\n",
+       "      <th>level</th>\n",
+       "      <th>dataDate</th>\n",
+       "      <th>dataTime</th>\n",
+       "      <th>stepRange</th>\n",
+       "      <th>dataType</th>\n",
+       "      <th>number</th>\n",
+       "      <th>gridType</th>\n",
+       "    </tr>\n",
+       "  </thead>\n",
+       "  <tbody>\n",
+       "    <tr>\n",
+       "      <th>0</th>\n",
+       "      <td>ecmf</td>\n",
+       "      <td>t</td>\n",
+       "      <td>isobaricInhPa</td>\n",
+       "      <td>500</td>\n",
+       "      <td>20070101</td>\n",
+       "      <td>1200</td>\n",
+       "      <td>0</td>\n",
+       "      <td>an</td>\n",
+       "      <td>0</td>\n",
+       "      <td>regular_ll</td>\n",
+       "    </tr>\n",
+       "  </tbody>\n",
+       "</table>\n",
+       "</div>"
+      ],
+      "text/plain": [
+       "  centre shortName    typeOfLevel  level  dataDate  dataTime stepRange  \\\n",
+       "0   ecmf         t  isobaricInhPa    500  20070101      1200         0   \n",
+       "\n",
+       "  dataType  number    gridType  \n",
+       "0       an       0  regular_ll  "
+      ]
+     },
+     "execution_count": 1,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "import earthkit.data as ekd\n",
+    "\n",
+    "ds = ekd.from_source(\"file\", \"test4.grib\")\n",
+    "f_ori = ds[0]\n",
+    "f_ori.ls()"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "abf1ffe6-7c49-4cbc-b5c7-a267a5d6a68e",
+   "metadata": {
+    "editable": true,
+    "slideshow": {
+     "slide_type": ""
+    },
+    "tags": []
+   },
+   "source": [
+    "### Using clone()"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "cfc1e1d7-9220-4e8f-ba55-ad6928432b55",
+   "metadata": {
+    "editable": true,
+    "slideshow": {
+     "slide_type": ""
+    },
+    "tags": []
+   },
+   "source": [
+    "#### Modifying metadata"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 2,
+   "id": "939aa250-f5bb-4752-82e1-2f9327c3761f",
+   "metadata": {
+    "editable": true,
+    "slideshow": {
+     "slide_type": ""
+    },
+    "tags": []
+   },
+   "outputs": [
+    {
+     "data": {
+      "text/html": [
+       "<div>\n",
+       "<style scoped>\n",
+       "    .dataframe tbody tr th:only-of-type {\n",
+       "        vertical-align: middle;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe tbody tr th {\n",
+       "        vertical-align: top;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe thead th {\n",
+       "        text-align: right;\n",
+       "    }\n",
+       "</style>\n",
+       "<table border=\"1\" class=\"dataframe\">\n",
+       "  <thead>\n",
+       "    <tr style=\"text-align: right;\">\n",
+       "      <th></th>\n",
+       "      <th>centre</th>\n",
+       "      <th>shortName</th>\n",
+       "      <th>typeOfLevel</th>\n",
+       "      <th>level</th>\n",
+       "      <th>dataDate</th>\n",
+       "      <th>dataTime</th>\n",
+       "      <th>stepRange</th>\n",
+       "      <th>dataType</th>\n",
+       "      <th>number</th>\n",
+       "      <th>gridType</th>\n",
+       "    </tr>\n",
+       "  </thead>\n",
+       "  <tbody>\n",
+       "    <tr>\n",
+       "      <th>0</th>\n",
+       "      <td>ecmf</td>\n",
+       "      <td>t</td>\n",
+       "      <td>isobaricInhPa</td>\n",
+       "      <td>700</td>\n",
+       "      <td>20070101</td>\n",
+       "      <td>1200</td>\n",
+       "      <td>0</td>\n",
+       "      <td>an</td>\n",
+       "      <td>0</td>\n",
+       "      <td>regular_ll</td>\n",
+       "    </tr>\n",
+       "  </tbody>\n",
+       "</table>\n",
+       "</div>"
+      ],
+      "text/plain": [
+       "  centre shortName    typeOfLevel  level  dataDate  dataTime stepRange  \\\n",
+       "0   ecmf         t  isobaricInhPa    700  20070101      1200         0   \n",
+       "\n",
+       "  dataType  number    gridType  \n",
+       "0       an       0  regular_ll  "
+      ]
+     },
+     "execution_count": 2,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "f_new = f_ori.clone(level=700)\n",
+    "f_new.ls()"
+   ]
+  },
+  {
+   "cell_type": "raw",
+   "id": "b49e6507-c7e5-4081-9923-990b41692fd5",
+   "metadata": {
+    "editable": true,
+    "raw_mimetype": "text/restructuredtext",
+    "slideshow": {
+     "slide_type": ""
+    },
+    "tags": []
+   },
+   "source": [
+    "Please note that the metadata object in the new field is a wrapper around the original one and \n",
+    ":py:meth:`~data.readers.grib.metadata.GribMetadata.override` is not called to update it with the newly set keys. This means that there is no guarantee that the newly set keys are compatible with original metadata. So, arbitrary metadata keys can be set with no checks performed on their validity. This is generally not a problem for most of the field methods (e.g. :py:meth:`sel`) but can be an issue we we try to write the modified fields into a GRIB file (see below)."
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "5b47c4f6-56a3-4e30-ab33-fb00773723a3",
+   "metadata": {
+    "editable": true,
+    "slideshow": {
+     "slide_type": ""
+    },
+    "tags": []
+   },
+   "source": [
+    "To demonstrate what was said above we set \"level\" to an invalid value and add a custom (non GRIB) key:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 3,
+   "id": "af960845-678a-4e7b-9eb2-3cfd1b314655",
+   "metadata": {
+    "editable": true,
+    "slideshow": {
+     "slide_type": ""
+    },
+    "tags": []
+   },
+   "outputs": [
+    {
+     "data": {
+      "text/html": [
+       "<div>\n",
+       "<style scoped>\n",
+       "    .dataframe tbody tr th:only-of-type {\n",
+       "        vertical-align: middle;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe tbody tr th {\n",
+       "        vertical-align: top;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe thead th {\n",
+       "        text-align: right;\n",
+       "    }\n",
+       "</style>\n",
+       "<table border=\"1\" class=\"dataframe\">\n",
+       "  <thead>\n",
+       "    <tr style=\"text-align: right;\">\n",
+       "      <th></th>\n",
+       "      <th>centre</th>\n",
+       "      <th>shortName</th>\n",
+       "      <th>typeOfLevel</th>\n",
+       "      <th>level</th>\n",
+       "      <th>dataDate</th>\n",
+       "      <th>dataTime</th>\n",
+       "      <th>stepRange</th>\n",
+       "      <th>dataType</th>\n",
+       "      <th>number</th>\n",
+       "      <th>gridType</th>\n",
+       "      <th>my_key</th>\n",
+       "    </tr>\n",
+       "  </thead>\n",
+       "  <tbody>\n",
+       "    <tr>\n",
+       "      <th>0</th>\n",
+       "      <td>ecmf</td>\n",
+       "      <td>t</td>\n",
+       "      <td>isobaricInhPa</td>\n",
+       "      <td>abc</td>\n",
+       "      <td>20070101</td>\n",
+       "      <td>1200</td>\n",
+       "      <td>0</td>\n",
+       "      <td>an</td>\n",
+       "      <td>0</td>\n",
+       "      <td>regular_ll</td>\n",
+       "      <td>123</td>\n",
+       "    </tr>\n",
+       "  </tbody>\n",
+       "</table>\n",
+       "</div>"
+      ],
+      "text/plain": [
+       "  centre shortName    typeOfLevel level  dataDate  dataTime stepRange  \\\n",
+       "0   ecmf         t  isobaricInhPa   abc  20070101      1200         0   \n",
+       "\n",
+       "  dataType  number    gridType my_key  \n",
+       "0       an       0  regular_ll    123  "
+      ]
+     },
+     "execution_count": 3,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "f_new = f_ori.clone(level=\"abc\", my_key=\"123\")\n",
+    "f_new.ls(extra_keys=\"my_key\")"
+   ]
+  },
+  {
+   "cell_type": "raw",
+   "id": "fc9105b5-4eda-4a5d-9a6b-35ce699d43e7",
+   "metadata": {
+    "editable": true,
+    "raw_mimetype": "text/restructuredtext",
+    "slideshow": {
+     "slide_type": ""
+    },
+    "tags": []
+   },
+   "source": [
+    "Metadata values passed to :py:meth:`~data.core.fieldlist.Field.clone` can be callables."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 4,
+   "id": "a0b2a842-0b11-479a-8f7e-d2160e18c72e",
+   "metadata": {
+    "editable": true,
+    "slideshow": {
+     "slide_type": ""
+    },
+    "tags": []
+   },
+   "outputs": [
+    {
+     "data": {
+      "text/html": [
+       "<div>\n",
+       "<style scoped>\n",
+       "    .dataframe tbody tr th:only-of-type {\n",
+       "        vertical-align: middle;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe tbody tr th {\n",
+       "        vertical-align: top;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe thead th {\n",
+       "        text-align: right;\n",
+       "    }\n",
+       "</style>\n",
+       "<table border=\"1\" class=\"dataframe\">\n",
+       "  <thead>\n",
+       "    <tr style=\"text-align: right;\">\n",
+       "      <th></th>\n",
+       "      <th>centre</th>\n",
+       "      <th>shortName</th>\n",
+       "      <th>typeOfLevel</th>\n",
+       "      <th>level</th>\n",
+       "      <th>dataDate</th>\n",
+       "      <th>dataTime</th>\n",
+       "      <th>stepRange</th>\n",
+       "      <th>dataType</th>\n",
+       "      <th>number</th>\n",
+       "      <th>gridType</th>\n",
+       "      <th>custom_name</th>\n",
+       "    </tr>\n",
+       "  </thead>\n",
+       "  <tbody>\n",
+       "    <tr>\n",
+       "      <th>0</th>\n",
+       "      <td>ecmf</td>\n",
+       "      <td>t</td>\n",
+       "      <td>isobaricInhPa</td>\n",
+       "      <td>500</td>\n",
+       "      <td>20070101</td>\n",
+       "      <td>1200</td>\n",
+       "      <td>0</td>\n",
+       "      <td>an</td>\n",
+       "      <td>0</td>\n",
+       "      <td>regular_ll</td>\n",
+       "      <td>t500</td>\n",
+       "    </tr>\n",
+       "  </tbody>\n",
+       "</table>\n",
+       "</div>"
+      ],
+      "text/plain": [
+       "  centre shortName    typeOfLevel  level  dataDate  dataTime stepRange  \\\n",
+       "0   ecmf         t  isobaricInhPa    500  20070101      1200         0   \n",
+       "\n",
+       "  dataType  number    gridType custom_name  \n",
+       "0       an       0  regular_ll        t500  "
+      ]
+     },
+     "execution_count": 4,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "def _f(field, key, original_metadata):\n",
+    "    return original_metadata[\"param\"] + str(original_metadata[\"level\"])\n",
+    "\n",
+    "f_new = f_ori.clone(custom_name=_f)\n",
+    "f_new.ls(extra_keys=\"custom_name\")"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "5dbf44c9-8439-4da0-9caf-1c9c176c62ed",
+   "metadata": {
+    "editable": true,
+    "slideshow": {
+     "slide_type": ""
+    },
+    "tags": []
+   },
+   "source": [
+    "#### Modifying values"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 5,
+   "id": "51a4a474-afa4-48cd-b0ab-55ee295ddd92",
+   "metadata": {
+    "editable": true,
+    "slideshow": {
+     "slide_type": ""
+    },
+    "tags": []
+   },
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "(array([229.04600525, 229.04600525, 229.04600525]),\n",
+       " array([228.04600525, 228.04600525, 228.04600525]))"
+      ]
+     },
+     "execution_count": 5,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "f_new = f_ori.clone(values=f_ori.values + 1)\n",
+    "f_new.values[0:3], f_ori.values[0:3]"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "601f588b-1c90-431b-89de-889e55d1c521",
+   "metadata": {
+    "editable": true,
+    "slideshow": {
+     "slide_type": ""
+    },
+    "tags": []
+   },
+   "source": [
+    "#### Modifying both metadata and values"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 6,
+   "id": "0419a041-3972-415c-aab9-fbb1dbbfa642",
+   "metadata": {
+    "editable": true,
+    "slideshow": {
+     "slide_type": ""
+    },
+    "tags": []
+   },
+   "outputs": [
+    {
+     "data": {
+      "text/html": [
+       "<div>\n",
+       "<style scoped>\n",
+       "    .dataframe tbody tr th:only-of-type {\n",
+       "        vertical-align: middle;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe tbody tr th {\n",
+       "        vertical-align: top;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe thead th {\n",
+       "        text-align: right;\n",
+       "    }\n",
+       "</style>\n",
+       "<table border=\"1\" class=\"dataframe\">\n",
+       "  <thead>\n",
+       "    <tr style=\"text-align: right;\">\n",
+       "      <th></th>\n",
+       "      <th>centre</th>\n",
+       "      <th>shortName</th>\n",
+       "      <th>typeOfLevel</th>\n",
+       "      <th>level</th>\n",
+       "      <th>dataDate</th>\n",
+       "      <th>dataTime</th>\n",
+       "      <th>stepRange</th>\n",
+       "      <th>dataType</th>\n",
+       "      <th>number</th>\n",
+       "      <th>gridType</th>\n",
+       "    </tr>\n",
+       "  </thead>\n",
+       "  <tbody>\n",
+       "    <tr>\n",
+       "      <th>0</th>\n",
+       "      <td>ecmf</td>\n",
+       "      <td>t</td>\n",
+       "      <td>isobaricInhPa</td>\n",
+       "      <td>700</td>\n",
+       "      <td>20070101</td>\n",
+       "      <td>1200</td>\n",
+       "      <td>0</td>\n",
+       "      <td>an</td>\n",
+       "      <td>0</td>\n",
+       "      <td>regular_ll</td>\n",
+       "    </tr>\n",
+       "  </tbody>\n",
+       "</table>\n",
+       "</div>"
+      ],
+      "text/plain": [
+       "  centre shortName    typeOfLevel  level  dataDate  dataTime stepRange  \\\n",
+       "0   ecmf         t  isobaricInhPa    700  20070101      1200         0   \n",
+       "\n",
+       "  dataType  number    gridType  \n",
+       "0       an       0  regular_ll  "
+      ]
+     },
+     "execution_count": 6,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "f_new = f_ori.clone(values=f_ori.values + 1, level=700)\n",
+    "f_new.ls()"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 7,
+   "id": "bf72674e-dc3f-4533-aa89-ab54e9a11ca0",
+   "metadata": {
+    "editable": true,
+    "slideshow": {
+     "slide_type": ""
+    },
+    "tags": []
+   },
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "array([229.04600525, 229.04600525, 229.04600525])"
+      ]
+     },
+     "execution_count": 7,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "f_new.values[0:3]"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "21e1dc03-4c9b-46bc-9785-1b0c6b5b2691",
+   "metadata": {
+    "editable": true,
+    "slideshow": {
+     "slide_type": ""
+    },
+    "tags": []
+   },
+   "source": [
+    "#### Saving the modifield field into a GRIB file"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 8,
+   "id": "84aaadd9-901e-4b87-a886-a197b5b504eb",
+   "metadata": {
+    "editable": true,
+    "slideshow": {
+     "slide_type": ""
+    },
+    "tags": []
+   },
+   "outputs": [
+    {
+     "data": {
+      "text/html": [
+       "<div>\n",
+       "<style scoped>\n",
+       "    .dataframe tbody tr th:only-of-type {\n",
+       "        vertical-align: middle;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe tbody tr th {\n",
+       "        vertical-align: top;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe thead th {\n",
+       "        text-align: right;\n",
+       "    }\n",
+       "</style>\n",
+       "<table border=\"1\" class=\"dataframe\">\n",
+       "  <thead>\n",
+       "    <tr style=\"text-align: right;\">\n",
+       "      <th></th>\n",
+       "      <th>centre</th>\n",
+       "      <th>shortName</th>\n",
+       "      <th>typeOfLevel</th>\n",
+       "      <th>level</th>\n",
+       "      <th>dataDate</th>\n",
+       "      <th>dataTime</th>\n",
+       "      <th>stepRange</th>\n",
+       "      <th>dataType</th>\n",
+       "      <th>number</th>\n",
+       "      <th>gridType</th>\n",
+       "    </tr>\n",
+       "  </thead>\n",
+       "  <tbody>\n",
+       "    <tr>\n",
+       "      <th>0</th>\n",
+       "      <td>ecmf</td>\n",
+       "      <td>t</td>\n",
+       "      <td>isobaricInhPa</td>\n",
+       "      <td>700</td>\n",
+       "      <td>20070101</td>\n",
+       "      <td>1200</td>\n",
+       "      <td>0</td>\n",
+       "      <td>an</td>\n",
+       "      <td>0</td>\n",
+       "      <td>regular_ll</td>\n",
+       "    </tr>\n",
+       "  </tbody>\n",
+       "</table>\n",
+       "</div>"
+      ],
+      "text/plain": [
+       "  centre shortName    typeOfLevel  level  dataDate  dataTime stepRange  \\\n",
+       "0   ecmf         t  isobaricInhPa    700  20070101      1200         0   \n",
+       "\n",
+       "  dataType  number    gridType  \n",
+       "0       an       0  regular_ll  "
+      ]
+     },
+     "execution_count": 8,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "f_new.save(\"_modified_field.grib\")\n",
+    "ds_new = ekd.from_source(\"file\", \"_modified_field.grib\")\n",
+    "ds_new[0].ls()"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 9,
+   "id": "1805823b-9904-4e17-93d3-fafbad6df6f8",
+   "metadata": {
+    "editable": true,
+    "slideshow": {
+     "slide_type": ""
+    },
+    "tags": []
+   },
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "array([229.04600525, 229.04600525, 229.04600525])"
+      ]
+     },
+     "execution_count": 9,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "ds_new[0].values[0:3]"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "01626f16-47fe-4ad7-9db4-6ce031a208e2",
+   "metadata": {
+    "editable": true,
+    "slideshow": {
+     "slide_type": ""
+    },
+    "tags": []
+   },
+   "source": [
+    "#### Saving is not always possible"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "7905e7d5-887f-4fb9-b08f-1158ae7828d3",
+   "metadata": {
+    "editable": true,
+    "slideshow": {
+     "slide_type": ""
+    },
+    "tags": []
+   },
+   "source": [
+    "Saving the modified field into a GRIB is only possible if the modified metadata is GRIB compatible. To demonstrate this we add a custom metadata key called \"_level\"."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 10,
+   "id": "ee9caaf2-4626-4acb-a849-0d8460af5237",
+   "metadata": {
+    "editable": true,
+    "slideshow": {
+     "slide_type": ""
+    },
+    "tags": []
+   },
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "(700, 500)"
+      ]
+     },
+     "execution_count": 10,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "f_new = f_ori.clone(_level=700)\n",
+    "f_new.metadata(\"_level\", \"level\")"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "f280dbf1-d6a7-4d7b-83b1-bdee92aaf249",
+   "metadata": {
+    "editable": true,
+    "slideshow": {
+     "slide_type": ""
+    },
+    "tags": []
+   },
+   "source": [
+    "Writing to GRIB is not possible because \"_level\" is not a valid GRIB key and ecCodes raises an exception."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 11,
+   "id": "5baa362d-ea7d-4b41-ace9-0631dba2f756",
+   "metadata": {
+    "editable": true,
+    "slideshow": {
+     "slide_type": ""
+    },
+    "tags": []
+   },
+   "outputs": [
+    {
+     "name": "stderr",
+     "output_type": "stream",
+     "text": [
+      "Error setting _level=700\n",
+      "Key/value not found\n",
+      "Traceback (most recent call last):\n",
+      "  File \"/Users/cgr/git/earthkit-data/src/earthkit/data/utils/message.py\", line 273, in set\n",
+      "    return eccodes.codes_set(self._handle, name, value)\n",
+      "  File \"/Users/cgr/git/eccodes-python/gribapi/gribapi.py\", line 2140, in grib_set\n",
+      "    grib_set_long(msgid, key, value)\n",
+      "  File \"/Users/cgr/git/eccodes-python/gribapi/gribapi.py\", line 1006, in grib_set_long\n",
+      "    GRIB_CHECK(lib.grib_set_long(h, key.encode(ENC), value))\n",
+      "  File \"/Users/cgr/git/eccodes-python/gribapi/gribapi.py\", line 226, in GRIB_CHECK\n",
+      "    errors.raise_grib_error(errid)\n",
+      "  File \"/Users/cgr/git/eccodes-python/gribapi/errors.py\", line 381, in raise_grib_error\n",
+      "    raise ERROR_MAP[errid](errid)\n",
+      "gribapi.errors.KeyValueNotFoundError: Key/value not found\n"
+     ]
+    },
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "Key/value not found\n"
+     ]
+    }
+   ],
+   "source": [
+    "try:\n",
+    "    f_new.save(\"_modified_field1.grib\")\n",
+    "except Exception as e:\n",
+    "    print(e)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "bce26496-7c9b-4ea4-8660-bacd5ef06ef6",
+   "metadata": {
+    "editable": true,
+    "slideshow": {
+     "slide_type": ""
+    },
+    "tags": []
+   },
+   "source": [
+    "### Using copy()"
+   ]
+  },
+  {
+   "cell_type": "raw",
+   "id": "ff2c9b5a-f9ba-4c47-b3b2-db2c6790c9c0",
+   "metadata": {
+    "editable": true,
+    "raw_mimetype": "text/restructuredtext",
+    "slideshow": {
+     "slide_type": ""
+    },
+    "tags": []
+   },
+   "source": [
+    ":py:meth:`~data.core.fieldlist.Field.copy` performs a deep copy to generate an :py:class:`~data.sources.array_list.ArrayField` storing all the all the data in memory. \n",
+    "\n",
+    "When :py:meth:`~data.core.fieldlist.Field.copy` is called without arguments both the values and the Metadata object of the original field are copied into the new field. The latter copy is created by calling :py:meth:`~data.readers.grib.metadata.GribMetadata.override` on the original metadata object."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 12,
+   "id": "1265f65d-c39b-4b22-ab41-3d03d532e08d",
+   "metadata": {
+    "editable": true,
+    "slideshow": {
+     "slide_type": ""
+    },
+    "tags": []
+   },
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "(array([228.04600525, 228.04600525, 228.04600525]),\n",
+       " array([228.04600525, 228.04600525, 228.04600525]))"
+      ]
+     },
+     "execution_count": 12,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "f_new = f_ori.copy()\n",
+    "f_new.values[:3], f_ori.values[:3]"
+   ]
+  },
+  {
+   "cell_type": "raw",
+   "id": "de3c15ba-770b-4f79-a134-5086f5aad9a3",
+   "metadata": {
+    "editable": true,
+    "raw_mimetype": "text/restructuredtext",
+    "slideshow": {
+     "slide_type": ""
+    },
+    "tags": []
+   },
+   "source": [
+    "We can pass new values to :py:meth:`~data.core.fieldlist.Field.copy`."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 13,
+   "id": "43e186a1-195e-424c-b778-356da142e163",
+   "metadata": {
+    "editable": true,
+    "slideshow": {
+     "slide_type": ""
+    },
+    "tags": []
+   },
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "(array([229.04600525, 229.04600525, 229.04600525]),\n",
+       " array([228.04600525, 228.04600525, 228.04600525]))"
+      ]
+     },
+     "execution_count": 13,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "f_new = f_ori.copy(values=f_ori.values + 1)\n",
+    "f_new.values[:3], f_ori.values[:3]"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "51aae760-50e3-44be-aff6-06cc3b5ec641",
+   "metadata": {
+    "editable": true,
+    "slideshow": {
+     "slide_type": ""
+    },
+    "tags": []
+   },
+   "source": [
+    "We can also pass a new metadata object."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 14,
+   "id": "20fb42a2-8d98-43dd-a270-42710922b19f",
+   "metadata": {
+    "editable": true,
+    "slideshow": {
+     "slide_type": ""
+    },
+    "tags": []
+   },
+   "outputs": [
+    {
+     "data": {
+      "text/html": [
+       "<div>\n",
+       "<style scoped>\n",
+       "    .dataframe tbody tr th:only-of-type {\n",
+       "        vertical-align: middle;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe tbody tr th {\n",
+       "        vertical-align: top;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe thead th {\n",
+       "        text-align: right;\n",
+       "    }\n",
+       "</style>\n",
+       "<table border=\"1\" class=\"dataframe\">\n",
+       "  <thead>\n",
+       "    <tr style=\"text-align: right;\">\n",
+       "      <th></th>\n",
+       "      <th>centre</th>\n",
+       "      <th>shortName</th>\n",
+       "      <th>typeOfLevel</th>\n",
+       "      <th>level</th>\n",
+       "      <th>dataDate</th>\n",
+       "      <th>dataTime</th>\n",
+       "      <th>stepRange</th>\n",
+       "      <th>dataType</th>\n",
+       "      <th>number</th>\n",
+       "      <th>gridType</th>\n",
+       "    </tr>\n",
+       "  </thead>\n",
+       "  <tbody>\n",
+       "    <tr>\n",
+       "      <th>0</th>\n",
+       "      <td>ecmf</td>\n",
+       "      <td>t</td>\n",
+       "      <td>isobaricInhPa</td>\n",
+       "      <td>300</td>\n",
+       "      <td>20070101</td>\n",
+       "      <td>1200</td>\n",
+       "      <td>0</td>\n",
+       "      <td>an</td>\n",
+       "      <td>0</td>\n",
+       "      <td>regular_ll</td>\n",
+       "    </tr>\n",
+       "  </tbody>\n",
+       "</table>\n",
+       "</div>"
+      ],
+      "text/plain": [
+       "  centre shortName    typeOfLevel  level  dataDate  dataTime stepRange  \\\n",
+       "0   ecmf         t  isobaricInhPa    300  20070101      1200         0   \n",
+       "\n",
+       "  dataType  number    gridType  \n",
+       "0       an       0  regular_ll  "
+      ]
+     },
+     "execution_count": 14,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "f_new = f_ori.copy(metadata=f_ori.metadata().override(level=300))\n",
+    "f_new.ls()"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "c4d862b1-21c7-4c8c-9d80-c2510080ca01",
+   "metadata": {
+    "editable": true,
+    "slideshow": {
+     "slide_type": ""
+    },
+    "tags": []
+   },
+   "outputs": [],
+   "source": []
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "dev_ecc",
+   "language": "python",
+   "name": "dev_ecc"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 3
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython3",
+   "version": "3.10.13"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/docs/examples/index.rst b/docs/examples/index.rst
index 83056c5..83181e7 100644
--- a/docs/examples/index.rst
+++ b/docs/examples/index.rst
@@ -43,6 +43,7 @@ GRIB
     grib_indexing.ipynb
     grib_missing.ipynb
     grib_metadata_object.ipynb
+    grib_modification.ipynb
     numpy_fieldlist.ipynb
     grib_array_backends.ipynb
     grib_nearest_gridpoint.ipynb
diff --git a/src/earthkit/data/core/fieldlist.py b/src/earthkit/data/core/fieldlist.py
index 4d8d0b5..612b652 100644
--- a/src/earthkit/data/core/fieldlist.py
+++ b/src/earthkit/data/core/fieldlist.py
@@ -635,16 +635,6 @@ class Field(Base):
         else:
             return self._metadata.as_namespace(None)
 
-    @abstractmethod
-    def copy(self, **kwargs):
-        r"""Return a copy of the field.
-
-        Returns
-        -------
-        :obj:`Field`
-        """
-        self._not_implemented()
-
     def dump(self, namespace=all, **kwargs):
         r"""Generate dump with all the metadata keys belonging to ``namespace``.
 
@@ -735,18 +725,64 @@ class Field(Base):
 
         # return {name: metadata(name) for name in names}
 
-    def to_field(self, flatten=False, dtype=None, array_backend=None, values=None, **kwargs):
-        r"""Convert to a new :class:`Field`.
+    @abstractmethod
+    def clone(self, *, values=None, metadata=None, **kwargs):
+        r"""Create a new :class:`ClonedField` the with updated values and/or metadata.
 
         Parameters
         ----------
-        array_backend: str, module, :obj:`ArrayBackend`
-            Specifies the array backend for the generated :class:`Field`. The array
-            type must be supported by :class:`ArrayBackend`.
-
+        values: array-like or None
+            The values to be stored in the new :class:`ClonedField`. When it is ``None`` the resulting
+            :class:`ClonedField` will access the values from the original field.
+        metadata: dict, :class:`Metadata` or None
+            If it is a dictionary, it is merged with ``**kwargs`` and interpreted in the same way
+            as ``**kwargs``. If it is a :class:`Metadata` object, it is used as the new metadata. In
+            this case ``**kwargs`` cannot be used.
         **kwargs: dict, optional
-            ``kwargs`` are passed to :obj:`to_array` to
-            extract the field values the resulting object will store.
+            Keys and values to update the metadata with. Metadata values can also be ``callables``
+            with the following positional arguments: original_field, key, original_metadata.
+            The new :class:`ClonedField` will contain a reference to the original metadata object and
+            keys not present in ``kwargs`` will be accessed from the original field.
+
+        Returns
+        -------
+        :class:`ClonedField`
+            The new field with updated values and/or metadata keeping a
+            reference to the original field.
+
+        Raises
+        ------
+        ValueError
+            If ``metadata`` is a :class:`Metadata` object and ``**kwargs`` is not empty.
+
+        """
+        self.not_implemented()
+
+    def copy(self, *, values=None, flatten=False, dtype=None, array_backend=None, metadata=None):
+        r"""Create a new :class:`ArrayField` by copying the values and metadata.
+
+        Parameters
+        ----------
+        values: array-like or None
+            The values to be stored in the new :class:`Field`. When it is ``None`` the values
+            extracted from the original field by using :obj:`to_array` with ``flatten``, ``dtype``
+            and ``array_backend`` and copied to the new field.
+        flatten: bool
+            Control the shape of the values when they are extracted from the original field.
+            When ``True``, flatten the array, otherwise the field's shape is kept. Only used when
+            ``values`` is not provided.
+        dtype: str, array.dtype or None
+            Control the typecode or data-type of the values when they are extracted from
+            the original field. If :obj:`None`, the default type used by the underlying
+            data accessor is used. For GRIB it is ``float64``. Only used when  ``values``
+            is not provided.
+        array_backend: str, module or None
+            Control the array backend of the values when they are extracted from
+            the original field. If :obj:`None`, the underlying array format
+            of the field is used. Only used when ``values`` is not provided.
+        metadata: :class:`Metadata` or None
+            The metadata to be stored in the new :class:`Field`. When it is :obj:`None`
+            a copy of the metadata of the original field is used.
 
         Returns
         -------
@@ -761,9 +797,12 @@ class Field(Base):
                 array_backend=array_backend,
             )
 
+        if metadata is None:
+            metadata = self._metadata.override()
+
         return ArrayField(
             values,
-            self._metadata.override(**kwargs),
+            metadata,
         )
 
     def to_xarray(self, *args, **kwargs):
@@ -1618,7 +1657,7 @@ class FieldList(Index):
         dtype('float32')
 
         """
-        return self.from_fields([f.to_field(array_backend=array_backend, **kwargs) for f in self])
+        return self.from_fields([f.copy(array_backend=array_backend, **kwargs) for f in self])
 
     def cube(self, *args, **kwargs):
         from earthkit.data.indexing.cube import FieldCube
diff --git a/src/earthkit/data/indexing/fieldlist.py b/src/earthkit/data/indexing/fieldlist.py
index a2c1a52..8e9686e 100644
--- a/src/earthkit/data/indexing/fieldlist.py
+++ b/src/earthkit/data/indexing/fieldlist.py
@@ -9,6 +9,8 @@
 
 
 from earthkit.data.core.fieldlist import FieldList
+from earthkit.data.core.metadata import Metadata
+from earthkit.data.core.metadata import WrappedMetadata
 
 
 class SimpleFieldList(FieldList):
@@ -96,43 +98,36 @@ class WrappedField:
         return repr(self._field)
 
 
-# class NewDataField(WrappedField):
-#     def __init__(self, field, data):
-#         super().__init__(field)
-#         self._data = data
-#         self.shape = data.shape
-
-#     def to_numpy(self, flatten=False, dtype=None, index=None):
-#         data = self._data
-#         if dtype is not None:
-#             data = data.astype(dtype)
-#         if flatten:
-#             data = data.flatten()
-#         if index is not None:
-#             data = data[index]
-#         return data
-
-
-class NewFieldMetadataWrapper:
-    def __init__(self, field, **kwargs):
-        from earthkit.data.core.metadata import WrappedMetadata
-
-        self.__metadata = WrappedMetadata(field._metadata, extra=kwargs, owner=field)
+class ClonedFieldCore:
+    def __init__(self, field, values=None, metadata=None, **kwargs):
+        self._field = field
+        self.__values = values
 
-    @property
-    def _metadata(self):
-        return self.__metadata
+        if metadata is not None:
+            if isinstance(metadata, dict):
+                metadata = dict(**metadata)
+            elif isinstance(metadata, (Metadata, WrappedMetadata)):
+                if kwargs:
+                    raise ValueError("metadata and kwargs cannot be used together")
+            else:
+                raise ValueError(
+                    "metadata must be a dict, Metadata or WrappedMetadata, got %s" % type(metadata)
+                )
 
+        if metadata is None:
+            metadata = dict()
 
-class NewFieldWrapper:
-    def __init__(self, field, values=None, **kwargs):
-        self._field = field
-        self.__values = values
+        assert metadata is not None
 
         if kwargs:
-            from earthkit.data.core.metadata import WrappedMetadata
-
-            self.__metadata = WrappedMetadata(field._metadata, extra=kwargs, owner=field)
+            if isinstance(metadata, dict):
+                metadata.update(kwargs)
+
+        if metadata:
+            if isinstance(metadata, dict):
+                self.__metadata = WrappedMetadata(field._metadata, extra=metadata, owner=field)
+            else:
+                self.__metadata = metadata
         else:
             self.__metadata = field._metadata
 
@@ -149,7 +144,11 @@ class NewFieldWrapper:
         return self.__metadata
 
     def _has_new_values(self):
-        return self.__values is not None
+        return self._values
+
+    @property
+    def handle(self):
+        return self._metadata._handle
 
 
 # For backwards compatibility
diff --git a/src/earthkit/data/readers/grib/codes.py b/src/earthkit/data/readers/grib/codes.py
index c8fd72f..85fe95c 100644
--- a/src/earthkit/data/readers/grib/codes.py
+++ b/src/earthkit/data/readers/grib/codes.py
@@ -15,7 +15,7 @@ import eccodes
 import numpy as np
 
 from earthkit.data.core.fieldlist import Field
-from earthkit.data.indexing.fieldlist import NewFieldWrapper
+from earthkit.data.indexing.fieldlist import ClonedFieldCore
 from earthkit.data.readers.grib.metadata import GribFieldMetadata
 from earthkit.data.utils.message import CodesHandle
 from earthkit.data.utils.message import CodesMessagePositionIndex
@@ -325,13 +325,13 @@ class GribField(Field):
         """
         return self.handle.get_buffer()
 
-    def copy(self, **kwargs):
-        return NewGribField(self, **kwargs)
+    def clone(self, **kwargs):
+        return ClonedGribField(self, **kwargs)
 
 
-class NewGribField(NewFieldWrapper, GribField):
+class ClonedGribField(ClonedFieldCore, GribField):
     def __init__(self, field, **kwargs):
-        NewFieldWrapper.__init__(self, field, **kwargs)
+        ClonedFieldCore.__init__(self, field, **kwargs)
         self._handle = field._handle
         GribField.__init__(
             self,
diff --git a/src/earthkit/data/readers/grib/memory.py b/src/earthkit/data/readers/grib/memory.py
index c69345b..bd77c32 100644
--- a/src/earthkit/data/readers/grib/memory.py
+++ b/src/earthkit/data/readers/grib/memory.py
@@ -11,7 +11,7 @@ import logging
 
 import eccodes
 
-from earthkit.data.indexing.fieldlist import NewFieldWrapper
+from earthkit.data.indexing.fieldlist import ClonedFieldCore
 from earthkit.data.indexing.fieldlist import SimpleFieldList
 from earthkit.data.readers import Reader
 from earthkit.data.readers.grib.codes import GribCodesHandle
@@ -165,13 +165,13 @@ class GribFieldInMemory(GribField):
     def _release(self):
         self._handle = None
 
-    def copy(self, **kwargs):
-        return NewGribFieldInMemory(self, **kwargs)
+    def clone(self, **kwargs):
+        return ClonedGribFieldInMemory(self, **kwargs)
 
 
-class NewGribFieldInMemory(NewFieldWrapper, GribFieldInMemory):
+class ClonedGribFieldInMemory(ClonedFieldCore, GribFieldInMemory):
     def __init__(self, field, **kwargs):
-        NewFieldWrapper.__init__(self, field, **kwargs)
+        ClonedFieldCore.__init__(self, field, **kwargs)
         self._handle = field._handle
         GribFieldInMemory.__init__(
             self,
diff --git a/src/earthkit/data/readers/netcdf/field.py b/src/earthkit/data/readers/netcdf/field.py
index c8a55f1..9117121 100644
--- a/src/earthkit/data/readers/netcdf/field.py
+++ b/src/earthkit/data/readers/netcdf/field.py
@@ -17,7 +17,7 @@ from earthkit.data.core.fieldlist import Field
 from earthkit.data.core.geography import Geography
 from earthkit.data.core.metadata import MetadataAccessor
 from earthkit.data.core.metadata import RawMetadata
-from earthkit.data.indexing.fieldlist import NewFieldWrapper
+from earthkit.data.indexing.fieldlist import ClonedFieldCore
 from earthkit.data.utils.bbox import BoundingBox
 from earthkit.data.utils.dates import to_datetime
 
@@ -293,13 +293,13 @@ class XArrayField(Field):
 
         return tidy(self._ds[self._ds[self.variable].grid_mapping].attrs)
 
-    def copy(self, **kwargs):
-        return NewXarrayField(self, **kwargs)
+    def clone(self, **kwargs):
+        return ClonedXarrayField(self, **kwargs)
 
 
-class NewXarrayField(NewFieldWrapper, XArrayField):
+class ClonedXarrayField(ClonedFieldCore, XArrayField):
     def __init__(self, field, **kwargs):
-        NewFieldWrapper.__init__(self, field, **kwargs)
+        ClonedFieldCore.__init__(self, field, **kwargs)
         XArrayField.__init__(self, field.ds, field.variable, field.slices, field.non_dim_coords)
 
 
diff --git a/src/earthkit/data/sources/array_list.py b/src/earthkit/data/sources/array_list.py
index 01a0eb0..9fde5df 100644
--- a/src/earthkit/data/sources/array_list.py
+++ b/src/earthkit/data/sources/array_list.py
@@ -11,7 +11,7 @@ import logging
 import math
 
 from earthkit.data.core.fieldlist import Field
-from earthkit.data.indexing.fieldlist import NewFieldWrapper
+from earthkit.data.indexing.fieldlist import ClonedFieldCore
 from earthkit.data.utils.array import array_namespace
 
 LOG = logging.getLogger(__name__)
@@ -43,8 +43,12 @@ class ArrayField(Field):
         if metadata is not None:
             metadata = metadata._hide_internal_keys()
 
-        self.__metadata = metadata
-        self._array = array
+        self._metadata_ = metadata
+        self._array_ = array
+
+    @property
+    def _array(self):
+        return self._array_
 
     def _values(self, dtype=None):
         """Native array type"""
@@ -79,15 +83,15 @@ class ArrayField(Field):
 
     @property
     def _metadata(self):
-        return self.__metadata
+        return self._metadata_
 
     @property
     def handle(self):
         return self._metadata._handle
 
     def _release(self):
-        self._array = None
-        self.__metadata = None
+        self._array_ = None
+        self._metadata_ = None
 
     def __getstate__(self) -> dict:
         ret = {}
@@ -96,16 +100,16 @@ class ArrayField(Field):
         return ret
 
     def __setstate__(self, state: dict):
-        self._array = state.pop("_array")
-        self.__metadata = state.pop("_metadata")
+        self._array_ = state.pop("_array")
+        self._metadata_ = state.pop("_metadata")
 
-    def copy(self, **kwargs):
-        return NewArrayField(self, **kwargs)
+    def clone(self, **kwargs):
+        return ClonedArrayField(self, **kwargs)
 
 
-class NewArrayField(NewFieldWrapper, ArrayField):
+class ClonedArrayField(ClonedFieldCore, ArrayField):
     def __init__(self, field, **kwargs):
-        NewFieldWrapper.__init__(self, field, **kwargs)
+        ClonedFieldCore.__init__(self, field, **kwargs)
         ArrayField.__init__(self, field._array, None)
 
 
diff --git a/src/earthkit/data/sources/forcings.py b/src/earthkit/data/sources/forcings.py
index a8bb1cb..2495562 100644
--- a/src/earthkit/data/sources/forcings.py
+++ b/src/earthkit/data/sources/forcings.py
@@ -20,7 +20,7 @@ from earthkit.data.core.index import MaskIndex
 from earthkit.data.core.metadata import RawMetadata
 from earthkit.data.decorators import cached_method
 from earthkit.data.decorators import normalize
-from earthkit.data.indexing.fieldlist import NewFieldWrapper
+from earthkit.data.indexing.fieldlist import ClonedFieldCore
 from earthkit.data.utils.dates import to_datetime
 
 LOG = logging.getLogger(__name__)
@@ -255,16 +255,16 @@ class ForcingField(Field):
             values = values.astype(dtype)
         return values
 
-    def copy(self, **kwargs):
-        return NewForcingField(self, **kwargs)
+    def clone(self, **kwargs):
+        return ClonedForcingField(self, **kwargs)
 
     def __repr__(self):
         return "ForcingField(%s,%s,%s)" % (self.param, self.date, self.number)
 
 
-class NewForcingField(NewFieldWrapper, ForcingField):
+class ClonedForcingField(ClonedFieldCore, ForcingField):
     def __init__(self, field, **kwargs):
-        NewFieldWrapper.__init__(self, field, **kwargs)
+        ClonedFieldCore.__init__(self, field, **kwargs)
         ForcingField.__init__(self, field.maker, field.date, field.param, field.proc, number=field.number)
 
 


Test patch:
diff --git a/tests/grib/test_grib_copy.py b/tests/grib/test_grib_copy.py
index ad32162..c9df01d 100644
--- a/tests/grib/test_grib_copy.py
+++ b/tests/grib/test_grib_copy.py
@@ -26,7 +26,7 @@ from grib_fixtures import load_grib_data  # noqa: E402
 
 
 @pytest.mark.parametrize("fl_type", ["file", "array", "memory"])
-def test_grib_copy_metadata(fl_type):
+def test_grib_clone_metadata(fl_type):
     ds_ori, _ = load_grib_data("test4.grib", fl_type)
 
     def _func1(field, key, original_metadata):
@@ -45,7 +45,7 @@ def test_grib_copy_metadata(fl_type):
     # field
     # ---------------
 
-    f = ds_ori[0].copy(
+    f = ds_ori[0].clone(
         param="q",
         levelist=_func1,
         mars_area=_func2,
@@ -79,7 +79,7 @@ def test_grib_copy_metadata(fl_type):
     # write back to grib
     # we can only have ecCodes keys
     with temp_file() as tmp:
-        f = ds_ori[0].copy(
+        f = ds_ori[0].clone(
             param="q",
             levelist=_func1,
         )
@@ -95,14 +95,14 @@ def test_grib_copy_metadata(fl_type):
     # field - repeated use
     # ---------------------
 
-    f = ds_ori[0].copy(
+    f = ds_ori[0].clone(
         param="q",
         levelist=_func1,
         mars_area=_func2,
         name=_func3,
     )
 
-    f = f.copy(param="pt", levelist=_func4)
+    f = f.clone(param="pt", levelist=_func4)
 
     assert f.metadata("param") == "pt"
     assert f.metadata("shortName") == "t"
@@ -121,7 +121,7 @@ def test_grib_copy_metadata(fl_type):
 
     fields = []
     for i in range(2):
-        f = ds_ori[i].copy(
+        f = ds_ori[i].clone(
             param="q",
             levelist=_func1,
         )
@@ -155,7 +155,7 @@ def test_grib_copy_metadata(fl_type):
 
 
 @pytest.mark.parametrize("fl_type", ["file", "array", "memory"])
-def test_grib_copy_values(fl_type):
+def test_grib_clone_values(fl_type):
     ds_ori, _ = load_grib_data("test4.grib", fl_type)
 
     vals_ori = ds_ori[0].values
@@ -164,7 +164,7 @@ def test_grib_copy_values(fl_type):
     # field
     # ---------------
 
-    f = ds_ori[0].copy(values=vals_ori + 1)
+    f = ds_ori[0].clone(values=vals_ori + 1)
 
     assert f.metadata("param") == "t"
     assert f.metadata("shortName") == "t"
@@ -190,8 +190,8 @@ def test_grib_copy_values(fl_type):
     # field - repeated use
     # ---------------------
 
-    f = ds_ori[0].copy(values=vals_ori + 1)
-    f = f.copy(values=vals_ori + 2)
+    f = ds_ori[0].clone(values=vals_ori + 1)
+    f = f.clone(values=vals_ori + 2)
 
     assert f.metadata("param") == "t"
     assert f.metadata("shortName") == "t"
@@ -208,7 +208,7 @@ def test_grib_copy_values(fl_type):
 
     fields = []
     for i in range(2):
-        f = ds_ori[i].copy(values=vals_ori + i + 1)
+        f = ds_ori[i].clone(values=vals_ori + i + 1)
         fields.append(f)
 
     ds = FieldList.from_fields(fields)
@@ -243,7 +243,7 @@ def test_grib_copy_values(fl_type):
 
 
 @pytest.mark.parametrize("fl_type", ["file", "array", "memory"])
-def test_grib_copy_combined(fl_type):
+def test_grib_clone_combined(fl_type):
     ds_ori, _ = load_grib_data("test4.grib", fl_type)
 
     vals_ori = ds_ori[0].values
@@ -258,7 +258,7 @@ def test_grib_copy_combined(fl_type):
     # field
     # ---------------
 
-    f = ds_ori[0].copy(
+    f = ds_ori[0].clone(
         values=vals_ori + 1,
         param="q",
         levelist=_func1,
@@ -288,12 +288,12 @@ def test_grib_copy_combined(fl_type):
     # field - repeated use
     # ---------------------
 
-    f = ds_ori[0].copy(
+    f = ds_ori[0].clone(
         values=vals_ori + 1,
         param="q",
         levelist=_func1,
     )
-    f = f.copy(values=vals_ori + 2, param="pt", levelist=_func2)
+    f = f.clone(values=vals_ori + 2, param="pt", levelist=_func2)
 
     assert f.metadata("param") == "pt"
     assert f.metadata("shortName") == "t"
@@ -312,7 +312,7 @@ def test_grib_copy_combined(fl_type):
 
     fields = []
     for i in range(2):
-        f = ds_ori[i].copy(
+        f = ds_ori[i].clone(
             values=vals_ori + i + 1,
             param="q",
             levelist=_func1,
@@ -350,6 +350,72 @@ def test_grib_copy_combined(fl_type):
     # assert ds_1.metadata("levelist") == [600, 600]
 
 
+@pytest.mark.parametrize("fl_type", ["file", "array", "memory"])
+def test_grib_clone_default(fl_type):
+    ds_ori, _ = load_grib_data("test4.grib", fl_type)
+
+    vals_ori = ds_ori[0].values
+
+    # ---------------
+    # field
+    # ---------------
+
+    f = ds_ori[0].clone()
+
+    assert f.metadata("param") == "t"
+    assert f.metadata("shortName") == "t"
+    assert f.metadata("level") == 500
+    assert f.metadata("levelist") == 500
+    assert f.metadata("date", "param") == (20070101, "t")
+    assert f.metadata("param", "date") == ("t", 20070101)
+    assert np.allclose(f.values, vals_ori)
+    assert np.allclose(ds_ori[0].values, vals_ori)
+
+    # write back to grib
+    # we can only have ecCodes keys
+    with temp_file() as tmp:
+        f.save(tmp)
+        f_saved = from_source("file", tmp)[0]
+        assert f_saved.metadata("param") == "t"
+        assert f_saved.metadata("shortName") == "t"
+        assert f_saved.metadata("level") == 500
+        assert f_saved.metadata("levelist") == 500
+        assert np.allclose(f_saved.values, vals_ori)
+
+
+@pytest.mark.parametrize("fl_type", ["file", "array", "memory"])
+def test_grib_clone_with_metadata_object(fl_type):
+    ds_ori, _ = load_grib_data("test4.grib", fl_type)
+
+    vals_ori = ds_ori[0].values
+
+    md = ds_ori[0].metadata().override(shortName="q", level=600)
+
+    f = ds_ori[0].clone(metadata=md)
+
+    assert f.metadata("param") == "q"
+    assert f.metadata("shortName") == "q"
+    assert f.metadata("level") == 600
+    assert f.metadata("levelist") == 600
+    assert f.metadata("date", "param") == (20070101, "q")
+    assert f.metadata("param", "date") == ("q", 20070101)
+    assert np.allclose(f.values, vals_ori)
+    assert np.allclose(ds_ori[0].values, vals_ori)
+
+    # write back to grib
+    with temp_file() as tmp:
+        f.save(tmp)
+        f_saved = from_source("file", tmp)[0]
+        assert f_saved.metadata("param") == "q"
+        assert f_saved.metadata("shortName") == "q"
+        assert f_saved.metadata("level") == 600
+        assert f_saved.metadata("levelist") == 600
+        assert np.allclose(f_saved.values, vals_ori)
+
+    with pytest.raises(ValueError):
+        ds_ori[0].clone(metadata=md, param="q")
+
+
 @pytest.mark.parametrize("fl_type", ["file", "array", "memory"])
 def test_grib_copy_to_field(fl_type):
     ds_ori, _ = load_grib_data("test4.grib", fl_type)
@@ -360,10 +426,18 @@ def test_grib_copy_to_field(fl_type):
     # field
     # ---------------
 
-    f = ds_ori[0].to_field(
+    md = (
+        ds_ori[0]
+        .metadata()
+        .override(
+            shortName="q",
+            level=600,
+        )
+    )
+
+    f = ds_ori[0].copy(
         values=vals_ori + 1,
-        shortName="q",
-        level=600,
+        metadata=md,
     )
 
     assert isinstance(f, ArrayField)
@@ -393,10 +467,17 @@ def test_grib_copy_to_field(fl_type):
 
     fields = []
     for i in range(2):
-        f = ds_ori[i].to_field(
+        md = (
+            ds_ori[i]
+            .metadata()
+            .override(
+                shortName="q",
+                level=600,
+            )
+        )
+        f = ds_ori[i].copy(
             values=vals_ori + i + 1,
-            shortName="q",
-            level=600,
+            metadata=md,
         )
         assert isinstance(f, ArrayField)
         fields.append(f)
diff --git a/tests/grib/test_grib_serialise.py b/tests/grib/test_grib_serialise.py
index 670c629..c5adc5f 100644
--- a/tests/grib/test_grib_serialise.py
+++ b/tests/grib/test_grib_serialise.py
@@ -90,7 +90,7 @@ def test_grib_serialise_array_fieldlist(fl_type, representation):
     r = ds2.sel(param="2t")
     assert len(r) == 1
 
-    ds2[0]._array += 1
+    ds2[0]._array_ += 1
     v1 = ds[0]._array
 
     with temp_file() as tmp: