Repository: DHI/mikeio
Pull request #592 (2023-08-10 15:42:33)

Type hints from the future
This line enables the union type hint syntax from 3.10 to be used in 3.7+
```python
from __future__ import annotations
```

I.e.

We can replace
```python
def foo(Union[int,str]) -> Union[int,bool):
  ...
```
with the version that is easier to read for humans🙂
```python
def foo(int | str) -> int | bool
  ...
```



Patch:
diff --git a/mikeio/_track.py b/mikeio/_track.py
index bf177a6d..34582780 100644
--- a/mikeio/_track.py
+++ b/mikeio/_track.py
@@ -1,6 +1,7 @@
+from __future__ import annotations
 import os
 from datetime import datetime
-from typing import Callable, Sequence, Tuple, Union
+from typing import Callable, Sequence, Tuple
 
 import numpy as np
 import pandas as pd
@@ -18,7 +19,7 @@ def _extract_track(
     end_time: datetime,
     timestep: float,
     geometry: GeometryFM2D,
-    track: Union[str, Dataset, pd.DataFrame],
+    track: str | Dataset | pd.DataFrame,
     items: Sequence[ItemInfo],
     item_numbers: Sequence[int],
     time_steps: Sequence[int],
diff --git a/mikeio/dataset/_data_utils.py b/mikeio/dataset/_data_utils.py
index 89049e83..49047743 100644
--- a/mikeio/dataset/_data_utils.py
+++ b/mikeio/dataset/_data_utils.py
@@ -1,6 +1,7 @@
+from __future__ import annotations
 import re
 from datetime import datetime
-from typing import Iterable, Sequence, Sized, Tuple, Union
+from typing import Iterable, Sequence, Sized, Tuple
 
 import numpy as np
 import pandas as pd
@@ -11,7 +12,7 @@ def _to_safe_name(name: str) -> str:
     return re.sub("_+", "_", tmp)  # Collapse multiple underscores
 
 
-def _n_selected_timesteps(x: Sized, k: Union[slice, Sized]) -> int:
+def _n_selected_timesteps(x: Sized, k: slice | Sized) -> int:
     if isinstance(k, slice):
         k = list(range(*k.indices(len(x))))
     return len(k)
@@ -65,7 +66,7 @@ class DataUtilsMixin:
 
     @staticmethod
     def _time_by_agg_axis(
-        time: pd.DatetimeIndex, axis: Union[int, Sequence[int]]
+        time: pd.DatetimeIndex, axis: int | Sequence[int]
     ) -> pd.DatetimeIndex:
         """New DatetimeIndex after aggregating over time axis"""
         if axis == 0 or (isinstance(axis, Sequence) and 0 in axis):
@@ -123,7 +124,7 @@ class DataUtilsMixin:
         return index
 
     @staticmethod
-    def _parse_axis(data_shape, dims, axis) -> Union[int, Tuple[int]]:
+    def _parse_axis(data_shape, dims, axis) -> int | Tuple[int]:
         # TODO change to return tuple always
         # axis = 0 if axis == "time" else axis
         if (axis == "spatial") or (axis == "space"):
@@ -175,7 +176,7 @@ class DataUtilsMixin:
         intime,
         outtime,
         data: np.ndarray,
-        method: Union[str, int],
+        method: str | int,
         extrapolate: bool,
         fill_value: float,
     ):
diff --git a/mikeio/dataset/_dataarray.py b/mikeio/dataset/_dataarray.py
index db1ac1d5..a902a35a 100644
--- a/mikeio/dataset/_dataarray.py
+++ b/mikeio/dataset/_dataarray.py
@@ -1,8 +1,10 @@
+from __future__ import annotations
 import warnings
 from copy import deepcopy
 from datetime import datetime
 from functools import cached_property
-from typing import Iterable, Optional, Sequence, Tuple, Union
+from typing import Iterable, Optional, Sequence, Tuple
+
 
 import numpy as np
 import pandas as pd
@@ -104,7 +106,7 @@ class DataArray(DataUtilsMixin):
         self,
         data,
         *,
-        time: Optional[Union[pd.DatetimeIndex, str]] = None,
+        time: Optional[pd.DatetimeIndex | str] = None,
         item: Optional[ItemInfo] = None,
         geometry=GeometryUndefined(),
         zn=None,
@@ -719,7 +721,7 @@ class DataArray(DataUtilsMixin):
     def sel(
         self,
         *,
-        time: Optional[Union[str, pd.DatetimeIndex, "DataArray"]] = None,
+        time: Optional[str | pd.DatetimeIndex | "DataArray"] = None,
         **kwargs,
     ) -> "DataArray":
         """Return a new DataArray whose data is given by
@@ -738,7 +740,7 @@ class DataArray(DataUtilsMixin):
 
         Parameters
         ----------
-        time : Union[str, pd.DatetimeIndex, DataArray], optional
+        time : str, pd.DatetimeIndex, DataArray, optional
             time labels e.g. "2018-01" or slice("2018-1-1","2019-1-1"),
             by default None
         x : float, optional
@@ -858,7 +860,7 @@ class DataArray(DataUtilsMixin):
         # TODO find out optimal syntax to allow interpolation to single point, new time, grid, mesh...
         self,
         # *, # TODO: make this a keyword-only argument in the future
-        time: Optional[Union[pd.DatetimeIndex, "DataArray"]] = None,
+        time: Optional[pd.DatetimeIndex | "DataArray"] = None,
         x: Optional[float] = None,
         y: Optional[float] = None,
         z: Optional[float] = None,
@@ -881,7 +883,7 @@ class DataArray(DataUtilsMixin):
 
         Parameters
         ----------
-        time : Union[float, pd.DatetimeIndex, DataArray], optional
+        time : float, pd.DatetimeIndex or DataArray, optional
             timestep in seconds or discrete time instances given by
             pd.DatetimeIndex (typically from another DataArray
             da2.time), by default None (=don't interp in time)
@@ -916,9 +918,7 @@ class DataArray(DataUtilsMixin):
         if z is not None:
             raise NotImplementedError()
 
-        geometry: Union[
-            GeometryPoint2D, GeometryPoint3D, GeometryUndefined
-        ] = GeometryUndefined()
+        geometry: GeometryPoint2D | GeometryPoint3D | GeometryUndefined = GeometryUndefined()
 
         # interp in space
         if (x is not None) or (y is not None) or (z is not None):
@@ -1022,7 +1022,7 @@ class DataArray(DataUtilsMixin):
 
     def interp_time(
         self,
-        dt: Union[float, pd.DatetimeIndex, "DataArray"],
+        dt: float | pd.DatetimeIndex | "DataArray",
         *,
         method="linear",
         extrapolate=True,
@@ -1101,7 +1101,7 @@ class DataArray(DataUtilsMixin):
 
     def interp_like(
         self,
-        other: Union["DataArray", Grid2D, GeometryFM2D, pd.DatetimeIndex],
+        other: "DataArray" | Grid2D | GeometryFM2D | pd.DatetimeIndex,
         interpolant=None,
         **kwargs,
     ) -> "DataArray":
@@ -1712,7 +1712,7 @@ class DataArray(DataUtilsMixin):
         self._to_dataset().to_dfs(filename, **kwargs)
 
     def to_dataframe(
-        self, *, unit_in_name: bool = False, round_time: Union[str, bool] = "ms"
+        self, *, unit_in_name: bool = False, round_time: str | bool = "ms"
     ) -> pd.DataFrame:
         """Convert to DataFrame
 
diff --git a/mikeio/dataset/_dataset.py b/mikeio/dataset/_dataset.py
index dc6ad63b..a5577661 100644
--- a/mikeio/dataset/_dataset.py
+++ b/mikeio/dataset/_dataset.py
@@ -1,3 +1,4 @@
+from __future__ import annotations
 import os
 import warnings
 from copy import deepcopy
@@ -9,7 +10,6 @@ from typing import (
     Optional,
     Sequence,
     Tuple,
-    Union,
     MutableMapping,
     Any,
 )
@@ -79,7 +79,7 @@ class Dataset(MutableMapping):
 
     def __init__(
         self,
-        data: Union[Mapping[str, DataArray], Iterable[DataArray], Sequence[np.ndarray]],
+        data: Mapping[str, DataArray] | Iterable[DataArray] | Sequence[np.ndarray],
         time=None,
         items=None,
         geometry=None,
@@ -579,7 +579,7 @@ class Dataset(MutableMapping):
             time_steps = list(range(s.start, s.stop))
             return self.isel(time_steps, axis=0)
 
-    def remove(self, key: Union[int, str]):
+    def remove(self, key: int | str):
         """Remove DataArray from Dataset
 
         Parameters
@@ -649,7 +649,7 @@ class Dataset(MutableMapping):
             self.__itemattr.remove(name)
             delattr(self, name)
 
-    def __getitem__(self, key) -> Union[DataArray, "Dataset"]:
+    def __getitem__(self, key) -> DataArray | "Dataset":
 
         # select time steps
         if (
@@ -838,7 +838,7 @@ class Dataset(MutableMapping):
 
         Parameters
         ----------
-        time : Union[str, pd.DatetimeIndex, Dataset], optional
+        time : str, pd.DatetimeIndex or Dataset, optional
             time labels e.g. "2018-01" or slice("2018-1-1","2019-1-1"),
             by default None
         x : float, optional
@@ -887,7 +887,7 @@ class Dataset(MutableMapping):
     def interp(
         self,
         *,
-        time: Optional[Union[pd.DatetimeIndex, "DataArray"]] = None,
+        time: Optional[pd.DatetimeIndex | "DataArray"] = None,
         x: Optional[float] = None,
         y: Optional[float] = None,
         z: Optional[float] = None,
@@ -909,7 +909,7 @@ class Dataset(MutableMapping):
 
         Parameters
         ----------
-        time : Union[float, pd.DatetimeIndex, Dataset], optional
+        time : float, pd.DatetimeIndex or Dataset, optional
             timestep in seconds or discrete time instances given by
             pd.DatetimeIndex (typically from another Dataset
             da2.time), by default None (=don't interp in time)
@@ -1022,7 +1022,7 @@ class Dataset(MutableMapping):
 
     def interp_time(
         self,
-        dt: Optional[Union[float, pd.DatetimeIndex, "Dataset", DataArray]] = None,
+        dt: Optional[float | pd.DatetimeIndex | "Dataset" | DataArray] = None,
         *,
         freq: Optional[str] = None,
         method="linear",
@@ -1100,7 +1100,7 @@ class Dataset(MutableMapping):
 
     def interp_like(
         self,
-        other: Union["Dataset", DataArray, Grid2D, GeometryFM2D, pd.DatetimeIndex],
+        other: "Dataset" | DataArray | Grid2D | GeometryFM2D | pd.DatetimeIndex,
         **kwargs,
     ) -> "Dataset":
         """Interpolate in space (and in time) to other geometry (and time axis)
@@ -1305,7 +1305,7 @@ class Dataset(MutableMapping):
 
     def aggregate(
         self, axis=0, func=np.nanmean, **kwargs
-    ) -> Union["Dataset", "DataArray"]:
+    ) -> "Dataset":
         """Aggregate along an axis
 
         Parameters
@@ -1324,15 +1324,8 @@ class Dataset(MutableMapping):
             if self.n_items <= 1:
                 return self
 
-            if "keepdims" in kwargs:
-                warnings.warn(
-                    "The keepdims arguments is deprecated. The result will always be a Dataset.",
-                    FutureWarning,
-                )
-
-            keepdims = kwargs.pop("keepdims", False)
             name = kwargs.pop("name", func.__name__)
-            data = func(self.to_numpy(), axis=0, keepdims=False, **kwargs)
+            data = func(self.to_numpy(), axis=0, **kwargs)
             item = self._agg_item_from_items(self.items, name)
             da = DataArray(
                 data=data,
@@ -1342,12 +1335,8 @@ class Dataset(MutableMapping):
                 dims=self.dims,
                 zn=self._zn,
             )
-            if not keepdims:
-                warnings.warn(
-                    "The keepdims arguments is deprecated. The result will always be a Dataset.",
-                    FutureWarning,
-                )
-            return Dataset([da], validate=False) if keepdims else da
+            
+            return Dataset([da], validate=False)
         else:
             res = {
                 name: da.aggregate(axis=axis, func=func, **kwargs)
@@ -1369,7 +1358,7 @@ class Dataset(MutableMapping):
         )
         return ItemInfo(name, it_type, it_unit)
 
-    def quantile(self, q, *, axis=0, **kwargs) -> Union["Dataset", "DataArray"]:
+    def quantile(self, q, *, axis=0, **kwargs) -> "Dataset":
         """Compute the q-th quantile of the data along the specified axis.
 
         Wrapping np.quantile
@@ -1399,7 +1388,7 @@ class Dataset(MutableMapping):
         """
         return self._quantile(q, axis=axis, func=np.quantile, **kwargs)
 
-    def nanquantile(self, q, *, axis=0, **kwargs) -> Union["Dataset", "DataArray"]:
+    def nanquantile(self, q, *, axis=0, **kwargs) -> "Dataset":
         """Compute the q-th quantile of the data along the specified axis, while ignoring nan values.
 
         Wrapping np.nanquantile
@@ -1427,14 +1416,13 @@ class Dataset(MutableMapping):
 
     def _quantile(
         self, q, *, axis=0, func=np.quantile, **kwargs
-    ) -> Union["Dataset", "DataArray"]:
+    ) -> "Dataset":
 
         if axis == "items":
-            keepdims = kwargs.pop("keepdims", False)
             if self.n_items <= 1:
                 return self  # or raise ValueError?
             if np.isscalar(q):
-                data = func(self.to_numpy(), q=q, axis=0, keepdims=False, **kwargs)
+                data = func(self.to_numpy(), q=q, axis=0, **kwargs)
                 item = self._agg_item_from_items(self.items, f"Quantile {str(q)}")
                 da = DataArray(
                     data=data,
@@ -1444,13 +1432,11 @@ class Dataset(MutableMapping):
                     dims=self.dims,
                     zn=self._zn,
                 )
-                return Dataset([da], validate=False) if keepdims else da
+                return Dataset([da], validate=False)
             else:
-                if keepdims:
-                    raise ValueError("Cannot keepdims for multiple quantiles")
-                res = []
+                res: List[DataArray] = []
                 for quantile in q:
-                    qd = self._quantile(q=quantile, axis=axis, func=func, **kwargs)
+                    qd = self._quantile(q=quantile, axis=axis, func=func, **kwargs)[0]
                     assert isinstance(qd, DataArray)
                     res.append(qd)
                 return Dataset(data=res, validate=False)
@@ -1470,7 +1456,7 @@ class Dataset(MutableMapping):
 
             return Dataset(data=res, validate=False)
 
-    def max(self, axis=0, **kwargs) -> Union["Dataset", "DataArray"]:
+    def max(self, axis=0, **kwargs) -> "Dataset":
         """Max value along an axis
 
         Parameters
@@ -1489,7 +1475,7 @@ class Dataset(MutableMapping):
         """
         return self.aggregate(axis=axis, func=np.max, **kwargs)
 
-    def min(self, axis=0, **kwargs) -> Union["Dataset", "DataArray"]:
+    def min(self, axis=0, **kwargs) -> "Dataset":
         """Min value along an axis
 
         Parameters
@@ -1508,7 +1494,7 @@ class Dataset(MutableMapping):
         """
         return self.aggregate(axis=axis, func=np.min, **kwargs)
 
-    def mean(self, axis=0, **kwargs) -> Union["Dataset", "DataArray"]:
+    def mean(self, axis=0, **kwargs) -> "Dataset":
         """Mean value along an axis
 
         Parameters
@@ -1528,7 +1514,7 @@ class Dataset(MutableMapping):
         """
         return self.aggregate(axis=axis, func=np.mean, **kwargs)
 
-    def std(self, axis=0, **kwargs) -> Union["Dataset", "DataArray"]:
+    def std(self, axis=0, **kwargs) -> "Dataset":
         """Standard deviation along an axis
 
         Parameters
@@ -1547,7 +1533,7 @@ class Dataset(MutableMapping):
         """
         return self.aggregate(axis=axis, func=np.std, **kwargs)
 
-    def ptp(self, axis=0, **kwargs) -> Union["Dataset", "DataArray"]:
+    def ptp(self, axis=0, **kwargs) -> "Dataset":
         """Range (max - min) a.k.a Peak to Peak along an axis
         Parameters
         ----------
@@ -1561,7 +1547,7 @@ class Dataset(MutableMapping):
         """
         return self.aggregate(axis=axis, func=np.ptp, **kwargs)
 
-    def average(self, weights, axis=0, **kwargs) -> Union["Dataset", "DataArray"]:
+    def average(self, weights, axis=0, **kwargs) -> "Dataset":
         """Compute the weighted average along the specified axis.
 
         Parameters
@@ -1595,7 +1581,7 @@ class Dataset(MutableMapping):
 
         return self.aggregate(axis=axis, func=func, **kwargs)
 
-    def nanmax(self, axis=0, **kwargs) -> Union["Dataset", "DataArray"]:
+    def nanmax(self, axis=0, **kwargs) -> "Dataset":
         """Max value along an axis (NaN removed)
 
         Parameters
@@ -1614,7 +1600,7 @@ class Dataset(MutableMapping):
         """
         return self.aggregate(axis=axis, func=np.nanmax, **kwargs)
 
-    def nanmin(self, axis=0, **kwargs) -> Union["Dataset", "DataArray"]:
+    def nanmin(self, axis=0, **kwargs) -> "Dataset":
         """Min value along an axis (NaN removed)
 
         Parameters
@@ -1629,7 +1615,7 @@ class Dataset(MutableMapping):
         """
         return self.aggregate(axis=axis, func=np.nanmin, **kwargs)
 
-    def nanmean(self, axis=0, **kwargs) -> Union["Dataset", "DataArray"]:
+    def nanmean(self, axis=0, **kwargs) -> "Dataset":
         """Mean value along an axis (NaN removed)
 
         Parameters
@@ -1644,7 +1630,7 @@ class Dataset(MutableMapping):
         """
         return self.aggregate(axis=axis, func=np.nanmean, **kwargs)
 
-    def nanstd(self, axis=0, **kwargs) -> Union["Dataset", "DataArray"]:
+    def nanstd(self, axis=0, **kwargs) -> "Dataset":
         """Standard deviation along an axis (NaN removed)
 
         Parameters
@@ -1760,7 +1746,7 @@ class Dataset(MutableMapping):
 
     # ===============================================
 
-    def to_pandas(self, **kwargs) -> Union[pd.Series, pd.DataFrame]:
+    def to_pandas(self, **kwargs) -> pd.Series | pd.DataFrame:
         """Convert Dataset to a Pandas DataFrame"""
 
         if self.n_items != 1:
@@ -1769,7 +1755,7 @@ class Dataset(MutableMapping):
             return self[0].to_pandas(**kwargs)
 
     def to_dataframe(
-        self, *, unit_in_name: bool = False, round_time: Union[str, bool] = "ms"
+        self, *, unit_in_name: bool = False, round_time: str | bool = "ms"
     ) -> pd.DataFrame:
         """Convert Dataset to a Pandas DataFrame
 
diff --git a/mikeio/dfs/_dfs.py b/mikeio/dfs/_dfs.py
index 58defc95..85d31a10 100644
--- a/mikeio/dfs/_dfs.py
+++ b/mikeio/dfs/_dfs.py
@@ -1,12 +1,12 @@
+from __future__ import annotations
 import warnings
 from abc import abstractmethod
-from datetime import datetime, timedelta
-from typing import Iterable, List, Optional, Tuple, Union, Sequence
+from datetime import datetime
+from typing import Iterable, List, Optional, Tuple, Sequence
 import numpy as np
 import pandas as pd
 from tqdm import tqdm, trange
 
-from copy import deepcopy
 from mikecore.DfsFactory import DfsFactory
 from mikecore.DfsFile import (
     DfsDynamicItemInfo,
@@ -70,7 +70,7 @@ def _fuzzy_item_search(
 
 def _valid_item_numbers(
     dfsItemInfo: List[DfsDynamicItemInfo],
-    items: Optional[Union[str, int, List[int], List[str]]] = None,
+    items: Optional[str | int | List[int] | List[str]] = None,
     ignore_first: bool = False,
 ) -> List[int]:
     start_idx = 1 if ignore_first else 0
diff --git a/mikeio/dfsu/_dfsu.py b/mikeio/dfsu/_dfsu.py
index b875ccf8..5e444062 100644
--- a/mikeio/dfsu/_dfsu.py
+++ b/mikeio/dfsu/_dfsu.py
@@ -1,17 +1,18 @@
+from __future__ import annotations
 import os
 import warnings
 from datetime import datetime, timedelta
 from functools import wraps
-from typing import Collection, List, Union, Optional, Tuple
+from typing import Collection, List, Optional, Tuple
 
 import numpy as np
 import pandas as pd
-from mikecore.DfsFactory import DfsFactory  # type: ignore
-from mikecore.DfsuBuilder import DfsuBuilder  # type: ignore
-from mikecore.DfsuFile import DfsuFile, DfsuFileType  # type: ignore
-from mikecore.eum import eumQuantity, eumUnit  # type: ignore
-from mikecore.MeshBuilder import MeshBuilder  # type: ignore
-from mikecore.MeshFile import MeshFile  # type: ignore
+from mikecore.DfsFactory import DfsFactory
+from mikecore.DfsuBuilder import DfsuBuilder
+from mikecore.DfsuFile import DfsuFile, DfsuFileType
+from mikecore.eum import eumQuantity, eumUnit
+from mikecore.MeshBuilder import MeshBuilder
+from mikecore.MeshFile import MeshFile
 from tqdm import trange
 
 from mikeio.spatial._utils import xy_to_bbox
@@ -1183,7 +1184,7 @@ class _Dfsu(_UnstructuredFile):
             self._dfs.Close()
             os.remove(filename)
 
-    def append(self, data: Union[List[np.ndarray], Dataset]) -> None:
+    def append(self, data: List[np.ndarray] | Dataset) -> None:
         """Append to a dfsu file opened with `write(...,keep_open=True)`
 
         Parameters
@@ -1201,7 +1202,7 @@ class _Dfsu(_UnstructuredFile):
                 zn = self.geometry.node_coordinates[:, 2]
                 self._dfs.WriteItemTimeStepNext(0, zn.astype(np.float32))
             for item in range(n_items):
-                dai: Union[np.ndarray, DataArray, Dataset] = data[item]
+                dai: np.ndarray | DataArray | Dataset = data[item]
                 if isinstance(dai, DataArray):
                     di: np.ndarray = dai.to_numpy()
                 elif isinstance(dai, np.ndarray):  # TODO is this too restrictive?
diff --git a/mikeio/dfsu/_spectral.py b/mikeio/dfsu/_spectral.py
index 9a9d5a52..3a9a99db 100644
--- a/mikeio/dfsu/_spectral.py
+++ b/mikeio/dfsu/_spectral.py
@@ -1,4 +1,5 @@
-from typing import Union, Tuple
+from __future__ import annotations
+from typing import Tuple
 
 import numpy as np
 import pandas as pd
@@ -321,7 +322,7 @@ class DfsuSpectral(_Dfsu):
         )
 
     def calc_Hm0_from_spectrum(
-        self, spectrum: Union[np.ndarray, DataArray], tail=True
+        self, spectrum: np.ndarray | DataArray, tail=True
     ) -> np.ndarray:
         """Calculate significant wave height (Hm0) from spectrum
 
diff --git a/mikeio/eum/_eum.py b/mikeio/eum/_eum.py
index 31ad5af6..9cb01b1f 100644
--- a/mikeio/eum/_eum.py
+++ b/mikeio/eum/_eum.py
@@ -16,9 +16,10 @@ degree Celsius
 >>>
 
 """
+from __future__ import annotations
 import warnings
 from enum import IntEnum
-from typing import Dict, List, Sequence, Union
+from typing import Dict, List, Sequence
 
 import pandas as pd
 from mikecore.DfsFile import DataValueType, DfsDynamicItemInfo
@@ -1521,7 +1522,7 @@ class ItemInfoList(list):
         return pd.DataFrame(data)
 
 
-def to_datatype(datatype: Union[str, int, DataValueType]) -> DataValueType:
+def to_datatype(datatype: str | int | DataValueType) -> DataValueType:
     string_datatype_mapping = {
         "Instantaneous": DataValueType.Instantaneous,
         "Accumulated": DataValueType.Accumulated,
diff --git a/mikeio/generic.py b/mikeio/generic.py
index 2072fdee..00d4bfd0 100644
--- a/mikeio/generic.py
+++ b/mikeio/generic.py
@@ -1,10 +1,11 @@
+from __future__ import annotations
 import math
 import os
 import pathlib
 from copy import deepcopy
 from datetime import datetime, timedelta
 from shutil import copyfile
-from typing import Iterable, List, Optional, Sequence, Union
+from typing import Iterable, List, Optional, Sequence
 
 import numpy as np
 import pandas as pd
@@ -88,8 +89,8 @@ class _ChunkInfo:
 
 
 def _clone(
-    infilename: Union[str, pathlib.Path],
-    outfilename: Union[str, pathlib.Path],
+    infilename: str | pathlib.Path,
+    outfilename: str | pathlib.Path,
     start_time=None,
     timestep=None,
     items=None,
@@ -98,9 +99,9 @@ def _clone(
 
     Parameters
     ----------
-    infilename : Union[str,pathlib.Path]
+    infilename : str | pathlib.Path
         input filename
-    outfilename : Union[str,pathlib.Path]
+    outfilename : str | pathlib.Path
         output filename
     start_time : datetime, optional
         new start time for the new file, by default None
@@ -191,20 +192,20 @@ def _clone(
 
 
 def scale(
-    infilename: Union[str, pathlib.Path],
-    outfilename: Union[str, pathlib.Path],
+    infilename: str | pathlib.Path,
+    outfilename: str | pathlib.Path,
     offset: float = 0.0,
     factor: float = 1.0,
-    items: Optional[Union[List[str], List[int]]] = None,
+    items: Optional[List[str] | List[int]] = None,
 ) -> None:
     """Apply scaling to any dfs file
 
     Parameters
     ----------
 
-    infilename: Union[str,pathlib.Path]
+    infilename: str | pathlib.Path
         full path to the input file
-    outfilename: Union[str,pathlib.Path]
+    outfilename: str | pathlib.Path
         full path to the output file
     offset: float, optional
         value to add to all items, default 0.0
@@ -244,10 +245,10 @@ def scale(
 
 
 def fill_corrupt(
-    infilename: Union[str, pathlib.Path],
-    outfilename: Union[str, pathlib.Path],
+    infilename: str | pathlib.Path,
+    outfilename: str | pathlib.Path,
     fill_value: float = np.nan,
-    items: Optional[Union[List[str], List[int]]] = None,
+    items: Optional[List[str] | List[int]] = None,
 ) -> None:
     """
     Replace corrupt (unreadable) data with fill_value, default delete value.
@@ -255,9 +256,9 @@ def fill_corrupt(
     Parameters
     ----------
 
-    infilename: Union[str,pathlib.Path]
+    infilename: str | pathlib.Path
         full path to the input file
-    outfilename: Union[str,pathlib.Path]
+    outfilename: str | pathlib.Path
         full path to the output file
     fill_value: float, optional
         value to use where data is corrupt, default delete value
@@ -306,19 +307,19 @@ def fill_corrupt(
 
 
 def sum(
-    infilename_a: Union[str, pathlib.Path],
-    infilename_b: Union[str, pathlib.Path],
-    outfilename: Union[str, pathlib.Path],
+    infilename_a: str | pathlib.Path,
+    infilename_b: str | pathlib.Path,
+    outfilename: str | pathlib.Path,
 ) -> None:
     """Sum two dfs files (a+b)
 
     Parameters
     ----------
-    infilename_a: Union[str,pathlib.Path]
+    infilename_a: str | pathlib.Path
         full path to the first input file
-    infilename_b: Union[str,pathlib.Path]
+    infilename_b: str | pathlib.Path
         full path to the second input file
-    outfilename: Union[str,pathlib.Path]
+    outfilename: str | pathlib.Path
         full path to the output file
     """
     infilename_a = str(infilename_a)
@@ -360,19 +361,19 @@ def sum(
 
 
 def diff(
-    infilename_a: Union[str, pathlib.Path],
-    infilename_b: Union[str, pathlib.Path],
-    outfilename: Union[str, pathlib.Path],
+    infilename_a: str | pathlib.Path,
+    infilename_b: str | pathlib.Path,
+    outfilename: str | pathlib.Path,
 ) -> None:
     """Calculate difference between two dfs files (a-b)
 
     Parameters
     ----------
-    infilename_a: Union[str,pathlib.Path]
+    infilename_a: str | pathlib.Path
         full path to the first input file
-    infilename_b: Union[str,pathlib.Path]
+    infilename_b: str | pathlib.Path
         full path to the second input file
-    outfilename: Union[str,pathlib.Path]
+    outfilename: str | pathlib.Path
         full path to the output file
     """
     infilename_a = str(infilename_a)
@@ -416,8 +417,8 @@ def diff(
 
 
 def concat(
-    infilenames: Sequence[Union[str, pathlib.Path]],
-    outfilename: Union[str, pathlib.Path],
+    infilenames: Sequence[str | pathlib.Path],
+    outfilename: str | pathlib.Path,
     keep="last",
 ) -> None:
     """Concatenates files along the time axis
@@ -428,9 +429,9 @@ def concat(
     ----------
     infilenames: List[str]
         filenames to concatenate
-    outfilename: Union[str,pathlib.Path]
+    outfilename: str | pathlib.Path
         filename of output
-    keep: Union[str,pathlib.Path]
+    keep: str | pathlib.Path
         either 'first' (keep older), 'last' (keep newer)
         or 'average' can be selected. By default 'last'
 
@@ -538,8 +539,8 @@ def concat(
 
 
 def extract(
-    infilename: Union[str, pathlib.Path],
-    outfilename: Union[str, pathlib.Path],
+    infilename: str | pathlib.Path,
+    outfilename: str | pathlib.Path,
     start=0,
     end=-1,
     step=1,
@@ -549,9 +550,9 @@ def extract(
 
     Parameters
     ----------
-    infilename : Union[str,pathlib.Path]
+    infilename : str | pathlib.Path
         path to input dfs file
-    outfilename : Union[str,pathlib.Path]
+    outfilename : str | pathlib.Path
         path to output dfs file
     start : int, float, str or datetime, optional
         start of extraction as either step, relative seconds
@@ -721,17 +722,17 @@ def _parse_step(dfs_i, step):
 
 
 def avg_time(
-    infilename: Union[str, pathlib.Path],
-    outfilename: Union[str, pathlib.Path],
+    infilename: str | pathlib.Path,
+    outfilename: str | pathlib.Path,
     skipna=True,
 ):
     """Create a temporally averaged dfs file
 
     Parameters
     ----------
-    infilename : Union[str,pathlib.Path]
+    infilename : str | pathlib.Path
         input filename
-    outfilename : Union[str,pathlib.Path]
+    outfilename : str | pathlib.Path
         output filename
     skipna : bool, optional
         exclude NaN/delete values when computing the result, default True
@@ -787,8 +788,8 @@ def avg_time(
 
 
 def quantile(
-    infilename: Union[str, pathlib.Path],
-    outfilename: Union[str, pathlib.Path],
+    infilename: str | pathlib.Path,
+    outfilename: str | pathlib.Path,
     q,
     *,
     items=None,
@@ -799,9 +800,9 @@ def quantile(
 
     Parameters
     ----------
-    infilename : Union[str,pathlib.Path]
+    infilename : str | pathlib.Path
         input filename
-    outfilename : Union[str,pathlib.Path]
+    outfilename : str | pathlib.Path
         output filename
     q: array_like of float
         Quantile or sequence of quantiles to compute,
diff --git a/mikeio/pfs/_pfsdocument.py b/mikeio/pfs/_pfsdocument.py
index 6112afa7..3921baf2 100644
--- a/mikeio/pfs/_pfsdocument.py
+++ b/mikeio/pfs/_pfsdocument.py
@@ -1,9 +1,10 @@
+from __future__ import annotations
 import re
 import warnings
 from collections import Counter
 from datetime import datetime
 from pathlib import Path
-from typing import Callable, Dict, List, Mapping, TextIO, Tuple, Union
+from typing import Callable, Dict, List, Mapping, TextIO, Tuple
 
 import yaml
 
@@ -81,7 +82,7 @@ class PfsDocument(PfsSection):
 
     def __init__(
         self,
-        data: Union[TextIO, PfsSection, Dict],
+        data: TextIO | PfsSection | Dict,
         *,
         encoding="cp1252",
         names=None,
@@ -141,15 +142,6 @@ class PfsDocument(PfsSection):
                 rvals.append(v)
         return rkeys, rvals
 
-    @property
-    def data(self) -> Union[PfsSection, List[PfsSection]]:
-        warnings.warn(
-            FutureWarning(
-                "The data attribute has been deprecated, please access the targets by their names instead."
-            )
-        )
-        return self.targets[0] if self.n_targets == 1 else self.targets
-
     @property
     def targets(self) -> List[PfsSection]:
         """List of targets (root sections)"""
diff --git a/mikeio/spatial/_FM_geometry.py b/mikeio/spatial/_FM_geometry.py
index 7fcb3481..df14bd4e 100644
--- a/mikeio/spatial/_FM_geometry.py
+++ b/mikeio/spatial/_FM_geometry.py
@@ -1,7 +1,8 @@
+from __future__ import annotations
 import warnings
 from collections import namedtuple
 from functools import cached_property
-from typing import Collection, Sequence, Union, Optional, List
+from typing import Collection, Sequence, Optional, List
 
 import numpy as np
 from mikecore.DfsuFile import DfsuFileType  # type: ignore
@@ -942,7 +943,7 @@ class GeometryFM2D(_GeometryFM):
 
     def isel(
         self, idx: Collection[int], keepdims=False, **kwargs
-    ) -> Union["GeometryFM2D", GeometryPoint2D]:
+    ) -> "GeometryFM2D" | GeometryPoint2D:
         """export a selection of elements to a new geometry
 
         Typically not called directly, but by Dataset/DataArray's
@@ -1059,7 +1060,7 @@ class GeometryFM2D(_GeometryFM):
 
         return np.where(mask)[0]
 
-    def _nodes_to_geometry(self, nodes) -> Union["GeometryFM2D", GeometryPoint2D]:
+    def _nodes_to_geometry(self, nodes) -> "GeometryFM2D" | GeometryPoint2D:
         """export a selection of nodes to new flexible file geometry
 
         Note: takes only the elements for which all nodes are selected
@@ -1103,8 +1104,8 @@ class GeometryFM2D(_GeometryFM):
         )
 
     def elements_to_geometry(
-        self, elements: Union[int, Collection[int]], keepdims=False
-    ) -> Union["GeometryFM2D", GeometryPoint2D]:
+        self, elements: int | Collection[int], keepdims=False
+    ) -> "GeometryFM2D" | GeometryPoint2D:
         """export a selection of elements to new flexible file geometry
 
         Parameters
diff --git a/mikeio/spatial/_FM_geometry_layered.py b/mikeio/spatial/_FM_geometry_layered.py
index f93d293b..38d13549 100644
--- a/mikeio/spatial/_FM_geometry_layered.py
+++ b/mikeio/spatial/_FM_geometry_layered.py
@@ -1,5 +1,6 @@
+from __future__ import annotations
 from functools import cached_property
-from typing import Collection, Sequence, Union, List
+from typing import Collection, Sequence, List
 
 import numpy as np
 from mikecore.DfsuFile import DfsuFileType  # type: ignore
@@ -74,7 +75,7 @@ class _GeometryFMLayered(_GeometryFM):
         )
 
     def elements_to_geometry(
-        self, elements: Union[int, Collection[int]], node_layers="all", keepdims=False
+        self, elements: int | Collection[int], node_layers="all", keepdims=False
     ):
         sel_elements: List[int]
 
diff --git a/mikeio/spatial/_grid_geometry.py b/mikeio/spatial/_grid_geometry.py
index 822b9821..8f82ba60 100644
--- a/mikeio/spatial/_grid_geometry.py
+++ b/mikeio/spatial/_grid_geometry.py
@@ -1,5 +1,6 @@
+from __future__ import annotations
 import warnings
-from typing import Optional, Sequence, Tuple, Union
+from typing import Optional, Sequence, Tuple
 from dataclasses import dataclass
 import numpy as np
 
@@ -183,7 +184,7 @@ class Grid1D(_Geometry):
 
     def isel(
         self, idx, axis=None
-    ) -> Union["Grid1D", GeometryPoint2D, GeometryPoint3D, GeometryUndefined]:
+    ) -> "Grid1D" | GeometryPoint2D | GeometryPoint3D | GeometryUndefined:
         """Get a subset geometry from this geometry
 
         Parameters
@@ -777,8 +778,8 @@ class Grid2D(_Geometry):
         return ii, jj
 
     def _bbox_to_index(
-        self, bbox: Union[Sequence[float], BoundingBox]
-    ) -> Union[Tuple[None, None], Tuple[range, range]]:
+        self, bbox: Tuple[float,float,float,float] | BoundingBox
+    ) -> Tuple[range, range]:
         """Find subarea within this geometry"""
         if not (len(bbox) == 4):
             raise ValueError(
@@ -787,8 +788,7 @@ class Grid2D(_Geometry):
 
         x0, y0, x1, y1 = bbox
         if x0 > self.x[-1] or y0 > self.y[-1] or x1 < self.x[0] or y1 < self.y[0]:
-            warnings.warn("No elements in bbox")
-            return None, None
+            raise ValueError("area is outside grid")
 
         mask = (self.x >= x0) & (self.x <= x1)
         ii = np.where(mask)[0]
@@ -801,8 +801,8 @@ class Grid2D(_Geometry):
         return i, j
 
     def isel(
-        self, idx, axis: Union[int, str]
-    ) -> Union["Grid2D", "Grid1D", "GeometryUndefined"]:
+        self, idx, axis: int | str
+    ) -> "Grid2D" | "Grid1D" | "GeometryUndefined":
         """Return a new geometry as a subset of Grid2D along the given axis."""
         if isinstance(axis, str):
             if axis == "y":
@@ -1244,7 +1244,7 @@ class Grid3D(_Geometry):
 
     def _geometry_for_layers(
         self, layers, keepdims=False
-    ) -> Union[Grid2D, "Grid3D", "GeometryUndefined"]:
+    ) -> Grid2D | "Grid3D" | "GeometryUndefined":
         if layers is None:
             return self
 
diff --git a/mikeio/xyz.py b/mikeio/xyz.py
index 23f0e1fd..a4eb4f97 100644
--- a/mikeio/xyz.py
+++ b/mikeio/xyz.py
@@ -1,10 +1,10 @@
+from __future__ import annotations
 from pathlib import Path
-from typing import Union
 
 import pandas as pd
 
 
-def read_xyz(filename: Union[str, Path]) -> pd.DataFrame:
+def read_xyz(filename: str | Path) -> pd.DataFrame:
 
     df = pd.read_csv(filename, sep="\t", header=None)
     if df.shape[1] == 1:
@@ -18,7 +18,7 @@ def read_xyz(filename: Union[str, Path]) -> pd.DataFrame:
     return df
 
 
-def dataframe_to_xyz(self, filename: Union[str, Path]) -> None:
+def dataframe_to_xyz(self, filename: str | Path) -> None:
     # TODO validation
     self.to_csv(filename, sep="\t", header=False, index=False)
 
diff --git a/pyproject.toml b/pyproject.toml
index f4fe3c7b..e3e0a28d 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -72,7 +72,7 @@ notebooks= [
 # mypy global options:
 
 [tool.mypy]
-python_version = "3.9"
+python_version = "3.10"
 warn_return_any = false
 allow_redefinition = true
 warn_unreachable = true
@@ -80,7 +80,7 @@ warn_unreachable = true
 [[tool.mypy.overrides]]
 module = [
     "mikecore.*",
-    "pandas",
+    "pandas.*",
     "matplotlib.*",
     "shapely.*",
     "mpl_toolkits.*",


Test patch:
diff --git a/tests/test_dataset.py b/tests/test_dataset.py
index ea31df83..24d8e8c2 100644
--- a/tests/test_dataset.py
+++ b/tests/test_dataset.py
@@ -914,20 +914,20 @@ def test_aggregate_across_items():
 
     ds = mikeio.read("tests/testdata/State_wlbc_north_err.dfs1")
 
-    with pytest.warns(FutureWarning):  # TODO: remove in 1.5.0
-        dam = ds.max(axis="items")
-
-    assert isinstance(dam, mikeio.DataArray)
-    assert dam.geometry == ds.geometry
-    assert dam.dims == ds.dims
-    assert dam.type == ds[-1].type
-
-    dsm = ds.mean(axis="items", keepdims=True)
+    dsm = ds.mean(axis="items")
 
     assert isinstance(dsm, mikeio.Dataset)
     assert dsm.geometry == ds.geometry
     assert dsm.dims == ds.dims
 
+    dsq = ds.quantile(q=[0.1,0.5,0.9], axis="items")
+    assert isinstance(dsq, mikeio.Dataset)
+    assert dsq[0].name == "Quantile 0.1"
+    assert dsq[1].name == "Quantile 0.5"
+    assert dsq[2].name == "Quantile 0.9"
+
+    # TODO allow name to be specified similar to below
+
 
 def test_aggregate_selected_items_dfsu_save_to_new_file(tmp_path):
 
@@ -935,10 +935,8 @@ def test_aggregate_selected_items_dfsu_save_to_new_file(tmp_path):
 
     assert ds.n_items == 5
 
-    with pytest.warns(FutureWarning):  # TODO: remove keepdims in 1.5.0
-        dsm = ds.max(
-            axis="items", keepdims=True, name="Max Water Level"
-        )  # add a nice name
+    
+    dsm = ds.max(axis="items", name="Max Water Level")  # add a nice name
     assert len(dsm) == 1
     assert dsm[0].name == "Max Water Level"
     assert dsm.geometry == ds.geometry