diff --git a/nxtomo/application/nxtomo.py b/nxtomo/application/nxtomo.py
index 06c909b7046d0dc7041c9c08a97e1bb832b3bd14..d9bcc6fd97d8738176d602f057d488a81cab4819 100644
--- a/nxtomo/application/nxtomo.py
+++ b/nxtomo/application/nxtomo.py
@@ -1,12 +1,13 @@
 """Define NXtomo application and related functions and classes"""
 
+from __future__ import annotations
+
 import logging
 import os
 from copy import deepcopy
 from datetime import datetime
 from functools import partial
 from operator import is_not
-from typing import Optional, Union
 
 import h5py
 import numpy
@@ -34,12 +35,12 @@ class NXtomo(NXobject):
     Class defining an NXTomo.
     His final goal is to save data to disk.
 
-    :param str node_name: node_name is used by the NXobject parent to order children when dumping it to file.
+    :param node_name: node_name is used by the NXobject parent to order children when dumping it to file.
                           has NXtomo is expected to be the highest object in the hierachy. node_name will only be used for saving if no `data_path` is provided when calling 'save' function.
-    :param Optional[NXobject] parent: parent of this NXobject. Most likely None for NXtomo
+    :param parent: parent of this NXobject. Most likely None for NXtomo
     """
 
-    def __init__(self, node_name: str = "", parent: Optional[NXobject] = None) -> None:
+    def __init__(self, node_name: str = "", parent: NXobject | None = None) -> None:
         if node_name not in (None, ""):
             deprecated_warning(
                 type_="parameter",
@@ -64,11 +65,11 @@ class NXtomo(NXobject):
         self._set_freeze(True)
 
     @property
-    def start_time(self) -> Optional[Union[datetime, str]]:
+    def start_time(self) -> datetime | str | None:
         return self._start_time
 
     @start_time.setter
-    def start_time(self, start_time: Optional[Union[datetime, str]]):
+    def start_time(self, start_time: datetime | str | None):
         if not isinstance(start_time, (type(None), datetime, str)):
             raise TypeError(
                 f"start_time is expected ot be an instance of datetime or None. Not {type(start_time)}"
@@ -76,11 +77,11 @@ class NXtomo(NXobject):
         self._start_time = start_time
 
     @property
-    def end_time(self) -> Optional[Union[datetime, str]]:
+    def end_time(self) -> datetime | str | None:
         return self._end_time
 
     @end_time.setter
-    def end_time(self, end_time: Optional[Union[datetime, str]]):
+    def end_time(self, end_time: datetime | str | None):
         if not isinstance(end_time, (type(None), datetime, str)):
             raise TypeError(
                 f"end_time is expected ot be an instance of datetime or None. Not {type(end_time)}"
@@ -88,11 +89,11 @@ class NXtomo(NXobject):
         self._end_time = end_time
 
     @property
-    def title(self) -> Optional[str]:
+    def title(self) -> str | None:
         return self._title
 
     @title.setter
-    def title(self, title: Optional[str]):
+    def title(self, title: str | None):
         if isinstance(title, numpy.ndarray):
             # handle diamond use case
             title = str(title)
@@ -103,11 +104,11 @@ class NXtomo(NXobject):
         self._title = title
 
     @property
-    def instrument(self) -> Optional[NXinstrument]:
+    def instrument(self) -> NXinstrument | None:
         return self._instrument
 
     @instrument.setter
-    def instrument(self, instrument: Optional[NXinstrument]) -> None:
+    def instrument(self, instrument: NXinstrument | None) -> None:
         if not isinstance(instrument, (type(None), NXinstrument)):
             raise TypeError(
                 f"instrument is expected ot be an instance of {NXinstrument} or None. Not {type(instrument)}"
@@ -115,11 +116,11 @@ class NXtomo(NXobject):
         self._instrument = instrument
 
     @property
-    def sample(self) -> Optional[NXsample]:
+    def sample(self) -> NXsample | None:
         return self._sample
 
     @sample.setter
-    def sample(self, sample: Optional[NXsample]):
+    def sample(self, sample: NXsample | None):
         if not isinstance(sample, (type(None), NXsample)):
             raise TypeError(
                 f"sample is expected ot be an instance of {NXsample} or None. Not {type(sample)}"
@@ -127,11 +128,11 @@ class NXtomo(NXobject):
         self._sample = sample
 
     @property
-    def control(self) -> Optional[NXmonitor]:
+    def control(self) -> NXmonitor | None:
         return self._control
 
     @control.setter
-    def control(self, control: Optional[NXmonitor]) -> None:
+    def control(self, control: NXmonitor | None) -> None:
         if not isinstance(control, (type(None), NXmonitor)):
             raise TypeError(
                 f"control is expected ot be an instance of {NXmonitor} or None. Not {type(control)}"
@@ -139,14 +140,14 @@ class NXtomo(NXobject):
         self._control = control
 
     @property
-    def energy(self) -> Optional[float]:
+    def energy(self) -> float | None:
         """
         incident energy in keV
         """
         return self._energy
 
     @energy.setter
-    def energy(self, energy: Optional[float]) -> None:
+    def energy(self, energy: float | None) -> None:
         if not isinstance(energy, (type(None), float)):
             raise TypeError(
                 f"energy is expected ot be an instance of {float} or None. Not {type(energy)}"
@@ -154,11 +155,11 @@ class NXtomo(NXobject):
         self._energy.value = energy
 
     @property
-    def group_size(self) -> Optional[int]:
+    def group_size(self) -> int | None:
         return self._group_size
 
     @group_size.setter
-    def group_size(self, group_size: Optional[int]):
+    def group_size(self, group_size: int | None):
         if not (
             isinstance(group_size, (type(None), int))
             or (numpy.isscalar(group_size) and not isinstance(group_size, (str, bytes)))
@@ -169,11 +170,11 @@ class NXtomo(NXobject):
         self._group_size = group_size
 
     @property
-    def bliss_original_files(self) -> Optional[tuple]:
+    def bliss_original_files(self) -> tuple | None:
         return self._bliss_original_files
 
     @bliss_original_files.setter
-    def bliss_original_files(self, files: Optional[Union[tuple, numpy.ndarray]]):
+    def bliss_original_files(self, files: tuple | numpy.ndarray | None):
         if isinstance(files, numpy.ndarray):
             files = tuple(files)
         if not isinstance(files, (type(None), tuple)):
@@ -185,8 +186,8 @@ class NXtomo(NXobject):
     @docstring(NXobject)
     def to_nx_dict(
         self,
-        nexus_path_version: Optional[float] = None,
-        data_path: Optional[str] = None,
+        nexus_path_version: float | None = None,
+        data_path: str | None = None,
     ) -> dict:
         if data_path is None:
             data_path = ""
@@ -305,9 +306,9 @@ class NXtomo(NXobject):
         """
         Load NXtomo instance from file_path and data_path
 
-        :param str file_path: hdf5 file path containing the NXtomo
-        :param str data_path: location of the NXtomo
-        :param str detector_data_as: how to load detector data. Can be:
+        :param file_path: hdf5 file path containing the NXtomo
+        :param data_path: location of the NXtomo
+        :param detector_data_as: how to load detector data. Can be:
                                      * "as_virtual_source": load it as h5py's VirtualGroup
                                      * "as_data_url": load it as silx's DataUrl
                                      * "as_numpy_array": load them as a numpy array (warning: can be memory consuming since all the data will be loaded)
@@ -387,7 +388,7 @@ class NXtomo(NXobject):
         Ensure some key datasets have the expected number of value
 
         :param NXtomo nx_tomo: nx_tomo to check
-        :param bool raises_error: if True then raise ValueError when some incoherent number of value are encounter (if missing will drop a warning only).
+        :param raises_error: if True then raise ValueError when some incoherent number of value are encounter (if missing will drop a warning only).
                                   if False then will drop warnings only
         """
         if not isinstance(nx_tomo, NXtomo):
@@ -498,7 +499,7 @@ class NXtomo(NXobject):
         """
         concatenate a tuple of NXobject into a single NXobject
 
-        :param tuple nx_objects:
+        :param nx_objects:
         :return: NXtomo instance which is the concatenation of the nx_objects
         """
         nx_objects = tuple(filter(partial(is_not, None), nx_objects))
@@ -601,7 +602,7 @@ class NXtomo(NXobject):
         self,
         file_path: str,
         data_path: str,
-        nexus_path_version: Optional[float] = None,
+        nexus_path_version: float | None = None,
         overwrite: bool = False,
     ) -> None:
         # Note: we overwrite save function for NXtomo in order to force 'data_path' to be provided.
@@ -624,9 +625,9 @@ class NXtomo(NXobject):
 
         Note: Darks and flat will not be affected by this sub selection
 
-        :param float start_angle: left bound to apply selection
-        :param float stop_angle: right bound to apply selection
-        :param bool copy: if True then copy the nx_tomo. Else `nx_tomo` will be affected by the modifications
+        :param start_angle: left bound to apply selection
+        :param stop_angle: right bound to apply selection
+        :param copy: if True then copy the nx_tomo. Else `nx_tomo` will be affected by the modifications
         """
         nx_tomo.check_can_select_from_rotation_angle()
         if copy:
@@ -648,7 +649,7 @@ class NXtomo(NXobject):
     def sub_select_from_angle_offset(
         nx_tomo,
         start_angle_offset: float,
-        angle_interval: Optional[float],
+        angle_interval: float | None,
         shift_angles: bool,
         copy=True,
     ):
@@ -657,11 +658,11 @@ class NXtomo(NXobject):
 
         Note: Darks and flat will not be affected by this sub selection
 
-        :param float start_angle_offset: offset to apply to start the selection. Expected in degree. Must be signed.
+        :param start_angle_offset: offset to apply to start the selection. Expected in degree. Must be signed.
                                          **The offset is always relative to the first projection angle value**
-        :param float angle_interval: interval covered by the selection. If None then will select until the end...
-        :param bool shift_angles: should we shift the angles of `-start_angle_offset` (once the selection is done)
-        :param bool copy: if True then copy the nx_tomo. Else `nx_tomo` will be affected by the modifications
+        :param angle_interval: interval covered by the selection. If None then will select until the end...
+        :param shift_angles: should we shift the angles of `-start_angle_offset` (once the selection is done)
+        :param copy: if True then copy the nx_tomo. Else `nx_tomo` will be affected by the modifications
         """
         nx_tomo.check_can_select_from_rotation_angle()
 
@@ -736,9 +737,8 @@ class NXtomo(NXobject):
         """
         return the list of 'Nxtomo' entries at the root level
 
-        :param str file_path:
+        :param file_path:
         :return: list of valid Nxtomo node (ordered alphabetically)
-        :rtype: tuple
 
         ..note: entries are sorted to insure consistency
         """
@@ -789,18 +789,18 @@ class NXtomo(NXobject):
 def copy_nxtomo_file(
     input_file: str,
     output_file: str,
-    entries: Optional[tuple],
+    entries: tuple | None,
     overwrite: bool = False,
     vds_resolution="update",
 ):
     """
     copy one or several NXtomo from a file to another file (solving relative links)
 
-    :param str input_file: nexus file for hich NXtomo have to be copied
-    :param str output_file: output file
-    :param optional[tuple] entries: entries to be copied. If set to None then all entries will be copied
-    :param bool overwrite: overwrite data path if already exists
-    :param str vds_resolution: How to solve virtual datasets. Options are:
+    :param input_file: nexus file for hich NXtomo have to be copied
+    :param output_file: output file
+    :param entries: entries to be copied. If set to None then all entries will be copied
+    :param overwrite: overwrite data path if already exists
+    :param vds_resolution: How to solve virtual datasets. Options are:
         * update: update Virtual source (relative) paths according to the new location of the file
         * remove: replace the virtual data source by copying directly the resulting dataset. Warning: in this case all the dataset will be load in memory
         In the future next option could be:
diff --git a/nxtomo/io.py b/nxtomo/io.py
index b4cd5a48937efa836894b4c1372668bdcbe28ef8..55ce407ab62a170b40a92a6da3247118cf9f7bd0 100644
--- a/nxtomo/io.py
+++ b/nxtomo/io.py
@@ -2,8 +2,9 @@
 some io utils to handle `nexus <https://manual.nexusformat.org/index.html>`_ and `hdf5 <https://www.hdfgroup.org/solutions/hdf5/>`_ with `h5py <https://www.h5py.org/>`_
 """
 
+from __future__ import annotations
+
 import logging
-from typing import Optional
 import os
 from contextlib import contextmanager
 import h5py._hl.selections as selection
@@ -19,7 +20,7 @@ _logger = logging.getLogger(__name__)
 _DEFAULT_SWMR_MODE = None
 
 
-def get_swmr_mode() -> Optional[bool]:
+def get_swmr_mode() -> bool | None:
     """
     Return True if the swmr should be used in the tomoools scope
     """
@@ -41,10 +42,10 @@ def check_virtual_sources_exist(fname, data_path):
     """
     Check that a virtual dataset points to actual data.
 
-    :param str fname: HDF5 file path
-    :param str data_path: Path within the HDF5 file
+    :param fname: HDF5 file path
+    :param data_path: Path within the HDF5 file
 
-    :return bool res: Whether the virtual dataset points to actual data.
+    :return res: Whether the virtual dataset points to actual data.
     """
     with hdf5_open(fname) as f:
         if data_path not in f:
@@ -66,13 +67,12 @@ def check_virtual_sources_exist(fname, data_path):
     return True
 
 
-def from_data_url_to_virtual_source(url: DataUrl, target_path: Optional[str]) -> tuple:
+def from_data_url_to_virtual_source(url: DataUrl, target_path: str | None) -> tuple:
     """
     convert a DataUrl to a set (as tuple) of h5py.VirtualSource
 
-    :param DataUrl url: url to be converted to a virtual source. It must target a 2D detector
+    :param url: url to be converted to a virtual source. It must target a 2D detector
     :return: (h5py.VirtualSource, tuple(shape of the virtual source), numpy.drype: type of the dataset associated with the virtual source)
-    :rtype: tuple
     """
     if not isinstance(url, DataUrl):
         raise TypeError(
@@ -115,9 +115,8 @@ def from_virtual_source_to_data_url(vs: h5py.VirtualSource) -> DataUrl:
     """
     convert a h5py.VirtualSource to a DataUrl
 
-    :param h5py.VirtualSource vs: virtual source to be converted to a DataUrl
+    :param vs: virtual source to be converted to a DataUrl
     :return: url
-    :rtype: DataUrl
     """
     if not isinstance(vs, h5py.VirtualSource):
         raise TypeError(
@@ -133,7 +132,7 @@ def cwd_context(new_cwd=None):
     create a context with 'new_cwd'.
 
     on entry update current working directory to 'new_cwd' and reset previous 'working_directory' at exit
-    :param Optional[str] new_cwd: current working directory to use in the context
+    :param new_cwd: current working directory to use in the context
     """
     try:
         curdir = os.getcwd()
@@ -156,10 +155,9 @@ def to_target_rel_path(file_path: str, target_path: str) -> str:
     cast file_path to a relative path according to target_path.
     This is used to deduce h5py.VirtualSource path
 
-    :param str file_path: file path to be moved to relative
-    :param str target_path: target used as 'reference' to get relative path
+    :param file_path: file path to be moved to relative
+    :param target_path: target used as 'reference' to get relative path
     :return: relative path of file_path compared to target_path
-    :rtype: str
     """
     if file_path == target_path or os.path.abspath(file_path) == os.path.abspath(
         target_path
diff --git a/nxtomo/nxobject/nxdetector.py b/nxtomo/nxobject/nxdetector.py
index 783efd4f837b3fc66775742003e84199e8d41a65..9c65155a721bd723aae6060a86cecfe2042d027f 100644
--- a/nxtomo/nxobject/nxdetector.py
+++ b/nxtomo/nxobject/nxdetector.py
@@ -2,10 +2,12 @@
 module for handling a `nxdetector <https://manual.nexusformat.org/classes/base_classes/NXdetector.html>`_
 """
 
+from __future__ import annotations
+
 import os
+from typing import Iterable
 from functools import partial
 from operator import is_not
-from typing import Iterable, Optional, Union
 import numpy
 
 import h5py
@@ -94,18 +96,18 @@ class NXdetector(NXobject):
     def __init__(
         self,
         node_name="detector",
-        parent: Optional[NXobject] = None,
-        field_of_view: Optional[FOV] = None,
-        expected_dim: Optional[tuple] = None,
+        parent: NXobject | None = None,
+        field_of_view: FOV | None = None,
+        expected_dim: tuple | None = None,
     ) -> None:
         """
         representation of `nexus nxdetector <https://manual.nexusformat.org/classes/base_classes/NXdetector.html>`_
         Detector of the acquisition.
 
-        :param str node_name: name of the detector in the hierarchy
-        :param Optional[NXObject] parent: parent in the nexus hierarchy
-        :param Optional[FOV] field_of_view: field of view of the detector - if know.
-        :param Optional[tuple] expected_dim: user can provide expected dimesions as a tuple of int to be checked when data is set
+        :param node_name: name of the detector in the hierarchy
+        :param parent: parent in the nexus hierarchy
+        :param field_of_view: field of view of the detector - if know.
+        :param expected_dim: user can provide expected dimensions as a tuple of int to be checked when data is set
         """
         super().__init__(node_name=node_name, parent=parent)
         self._set_freeze(False)
@@ -133,7 +135,7 @@ class NXdetector(NXobject):
         self._set_freeze(True)
 
     @property
-    def data(self) -> Optional[Union[numpy.ndarray, tuple]]:
+    def data(self) -> numpy.ndarray | tuple | None:
         """
         detector data (frames).
         can be None, a numpy array or a list of DataUrl xor h5py Virtual Source
@@ -141,7 +143,7 @@ class NXdetector(NXobject):
         return self._data
 
     @data.setter
-    def data(self, data: Optional[Union[numpy.ndarray, tuple]]):
+    def data(self, data: numpy.ndarray | tuple | None):
         if isinstance(data, (tuple, list)) or (
             isinstance(data, numpy.ndarray)
             and data.ndim == 1
@@ -172,7 +174,7 @@ class NXdetector(NXobject):
         self._data = data
 
     @property
-    def x_pixel_size(self) -> Optional[float]:
+    def x_pixel_size(self) -> float | None:
         """
         x pixel size as a field with a unit (get a value and a unit - default unit is SI).
         Know as 'x sample pixel size' in some application
@@ -180,7 +182,7 @@ class NXdetector(NXobject):
         return self._x_pixel_size
 
     @x_pixel_size.setter
-    def x_pixel_size(self, x_pixel_size: Optional[float]) -> None:
+    def x_pixel_size(self, x_pixel_size: float | None) -> None:
         if not isinstance(x_pixel_size, (type(None), float)):
             raise TypeError(
                 f"x_pixel_size is expected ot be an instance of {float} or None. Not {type(x_pixel_size)}"
@@ -188,7 +190,7 @@ class NXdetector(NXobject):
         self._x_pixel_size.value = x_pixel_size
 
     @property
-    def y_pixel_size(self) -> Optional[float]:
+    def y_pixel_size(self) -> float | None:
         """
         y pixel size as a field with a unit (get a value and a unit - default unit is SI).
         Know as 'y sample pixel size' in some application
@@ -196,7 +198,7 @@ class NXdetector(NXobject):
         return self._y_pixel_size
 
     @y_pixel_size.setter
-    def y_pixel_size(self, y_pixel_size: Optional[float]) -> None:
+    def y_pixel_size(self, y_pixel_size: float | None) -> None:
         if not isinstance(y_pixel_size, (type(None), float)):
             raise TypeError(
                 f"y_pixel_size is expected ot be an instance of {float} or None. Not {type(y_pixel_size)}"
@@ -218,7 +220,7 @@ class NXdetector(NXobject):
         return DetZFlipTransformation(flip=True) in self.transformations.transformations
 
     @x_flipped.setter
-    def x_flipped(self, flipped: Optional[bool]):
+    def x_flipped(self, flipped: bool | None):
         deprecated_warning(
             type_="property",
             name="x_flipped",
@@ -228,7 +230,7 @@ class NXdetector(NXobject):
         )
         self.set_transformation_from_x_flipped(flipped)
 
-    def set_transformation_from_x_flipped(self, flipped: Optional[bool]):
+    def set_transformation_from_x_flipped(self, flipped: bool | None):
         """Util function to set transformation from x_flipped simple bool.
         Used for backward compatibility and convenience.
         """
@@ -271,7 +273,7 @@ class NXdetector(NXobject):
         )
         self.set_transformation_from_y_flipped(flipped=flipped)
 
-    def set_transformation_from_y_flipped(self, flipped: Optional[bool]):
+    def set_transformation_from_y_flipped(self, flipped: bool | None):
         # WARNING: moving from two simple boolean to full NXtransformations make the old API very weak. It should be removed
         # soon (but we want to keep the API for at least one release). This is expected to fail except if you stick to {x,y} flips
         if isinstance(flipped, numpy.bool_):
@@ -288,14 +290,14 @@ class NXdetector(NXobject):
         self.transformations.add_transformation(DetYFlipTransformation(flip=flipped))
 
     @property
-    def distance(self) -> Optional[float]:
+    def distance(self) -> float | None:
         """
         sample / detector distance as a field with unit (default SI).
         """
         return self._distance
 
     @distance.setter
-    def distance(self, distance: Optional[float]) -> None:
+    def distance(self, distance: float | None) -> None:
         if not isinstance(distance, (type(None), float)):
             raise TypeError(
                 f"distance is expected ot be an instance of {float} or None. Not {type(distance)}"
@@ -303,40 +305,38 @@ class NXdetector(NXobject):
         self._distance.value = distance
 
     @property
-    def field_of_view(self) -> Optional[FieldOfView]:
+    def field_of_view(self) -> FieldOfView | None:
         """
         detector :class:`~nxtomo.nxobject.nxdetector.FieldOfView`
         """
         return self._field_of_view
 
     @field_of_view.setter
-    def field_of_view(
-        self, field_of_view: Optional[Union[FieldOfView, str, None]]
-    ) -> None:
+    def field_of_view(self, field_of_view: FieldOfView | str | None) -> None:
         if field_of_view is not None:
             field_of_view = FOV.from_value(field_of_view)
         self._field_of_view = field_of_view
 
     @property
-    def count_time(self) -> Optional[numpy.ndarray]:
+    def count_time(self) -> numpy.ndarray | None:
         """
         count time for each frame
         """
         return self._count_time
 
     @count_time.setter
-    def count_time(self, count_time: Optional[Iterable]):
+    def count_time(self, count_time: Iterable | None):
         self._count_time.value = cast_and_check_array_1D(count_time, "count_time")
 
     @property
-    def estimated_cor_from_motor(self) -> Optional[float]:
+    def estimated_cor_from_motor(self) -> float | None:
         """
         hint of center of rotation in pixel read from motor (when possible)
         """
         return self._estimated_cor_from_motor
 
     @estimated_cor_from_motor.setter
-    def estimated_cor_from_motor(self, estimated_cor_from_motor: Optional[float]):
+    def estimated_cor_from_motor(self, estimated_cor_from_motor: float | None):
         if not isinstance(estimated_cor_from_motor, (type(None), float)):
             raise TypeError(
                 f"estimated_cor_from_motor is expected to be None, or an instance of float. Not {type(estimated_cor_from_motor)}"
@@ -344,14 +344,14 @@ class NXdetector(NXobject):
         self._estimated_cor_from_motor = estimated_cor_from_motor
 
     @property
-    def image_key_control(self) -> Optional[numpy.ndarray]:
+    def image_key_control(self) -> numpy.ndarray | None:
         """
         :class:`~nxtomo.nxobject.nxdetector.ImageKey` for each frames
         """
         return self._image_key_control
 
     @image_key_control.setter
-    def image_key_control(self, control_image_key: Optional[Iterable]):
+    def image_key_control(self, control_image_key: Iterable | None):
         control_image_key = cast_and_check_array_1D(
             control_image_key, "control_image_key"
         )
@@ -364,7 +364,7 @@ class NXdetector(NXobject):
             )
 
     @property
-    def image_key(self) -> Optional[numpy.ndarray]:
+    def image_key(self) -> numpy.ndarray | None:
         """
         :class:`~nxtomo.nxobject.nxdetector.ImageKey` for each frames. Replace all :class:`~nxtomo.nxobject,nxdetector.ImageKey.ALIGNMENT` by :class:`~nxtomo.nxobject,nxdetector.ImageKey.PROJECTION` to fulfil nexus standard
         """
@@ -378,36 +378,36 @@ class NXdetector(NXobject):
             return control_image_key
 
     @property
-    def tomo_n(self) -> Optional[int]:
+    def tomo_n(self) -> int | None:
         """
         expected number of :class:`~nxtomo.nxobject,nxdetector.ImageKey.PROJECTION` frames
         """
         return self._tomo_n
 
     @tomo_n.setter
-    def tomo_n(self, tomo_n: Optional[int]):
+    def tomo_n(self, tomo_n: int | None):
         self._tomo_n = tomo_n
 
     @property
-    def group_size(self) -> Optional[int]:
+    def group_size(self) -> int | None:
         """
         number of acquisition for the dataset
         """
         return self._group_size
 
     @group_size.setter
-    def group_size(self, group_size: Optional[int]):
+    def group_size(self, group_size: int | None):
         self._group_size = group_size
 
     @property
-    def roi(self) -> Optional[tuple]:
+    def roi(self) -> tuple | None:
         """
         detector region of interest as x0,y0,x1,y1
         """
         return self._roi
 
     @roi.setter
-    def roi(self, roi: Optional[tuple]) -> None:
+    def roi(self, roi: tuple | None) -> None:
         if roi is None:
             self._roi = None
         elif not isinstance(roi, (tuple, list, numpy.ndarray)):
@@ -422,8 +422,8 @@ class NXdetector(NXobject):
     @docstring(NXobject)
     def to_nx_dict(
         self,
-        nexus_path_version: Optional[float] = None,
-        data_path: Optional[str] = None,
+        nexus_path_version: float | None = None,
+        data_path: str | None = None,
     ) -> dict:
         nexus_paths = get_nexus_path(nexus_path_version)
         nexus_detector_paths = nexus_paths.nx_detector_paths
@@ -504,8 +504,8 @@ class NXdetector(NXobject):
 
     def _data_to_nx_dict(
         self,
-        nexus_path_version: Optional[float] = None,
-        data_path: Optional[str] = None,
+        nexus_path_version: float | None = None,
+        data_path: str | None = None,
     ) -> dict:
         nexus_paths = get_nexus_path(nexus_path_version)
         nexus_detector_paths = nexus_paths.nx_detector_paths
@@ -920,20 +920,20 @@ class NXdetectorWithUnit(NXdetector):
         node_name="detector",
         parent=None,
         field_of_view=None,
-        expected_dim: Optional[tuple] = None,
+        expected_dim: tuple | None = None,
     ) -> None:
         super().__init__(node_name, parent, field_of_view, expected_dim)
         self._data = ElementWithUnit(default_unit=default_unit)
 
     @property
     @docstring(NXdetector)
-    def data(self) -> Union[numpy.ndarray, tuple]:
+    def data(self) -> numpy.ndarray | tuple:
         """data can be None, a numpy array or a list of DataUrl xor h5py Virtual Source"""
         return self._data
 
     @data.setter
     @docstring(NXdetector)
-    def data(self, data: Optional[Union[numpy.ndarray, tuple]]):
+    def data(self, data: numpy.ndarray | tuple | None):
         if isinstance(data, numpy.ndarray):
             if (
                 self._expected_dim is not None
@@ -960,8 +960,8 @@ class NXdetectorWithUnit(NXdetector):
 
     def _data_to_nx_dict(
         self,
-        nexus_path_version: Optional[float] = None,
-        data_path: Optional[str] = None,
+        nexus_path_version: float | None = None,
+        data_path: str | None = None,
     ) -> dict:
         nexus_paths = get_nexus_path(nexus_path_version)
         nexus_detector_paths = nexus_paths.nx_detector_paths
diff --git a/nxtomo/nxobject/nxinstrument.py b/nxtomo/nxobject/nxinstrument.py
index 3ac7c61121e5629b686ebd369c3deea3155a396d..b54b8d1b40118420d90ccbbc369a5610fb450aa6 100644
--- a/nxtomo/nxobject/nxinstrument.py
+++ b/nxtomo/nxobject/nxinstrument.py
@@ -2,10 +2,11 @@
 module for handling a `nxinstrument <https://manual.nexusformat.org/classes/base_classes/NXinstrument.html>`_
 """
 
+from __future__ import annotations
+
 import logging
 from functools import partial
 from operator import is_not
-from typing import Optional
 
 from silx.utils.proxy import docstring
 from silx.io.utils import open as open_hdf5
@@ -22,15 +23,15 @@ _logger = logging.getLogger(__name__)
 
 class NXinstrument(NXobject):
     def __init__(
-        self, node_name: str = "instrument", parent: Optional[NXobject] = None
+        self, node_name: str = "instrument", parent: NXobject | None = None
     ) -> None:
         """
         representation of `nexus NXinstrument <https://manual.nexusformat.org/classes/base_classes/NXinstrument.html>`_.
 
         Collection of the components of the instrument or beamline.
 
-        :param str node_name: name of the detector in the hierarchy
-        :param Optional[NXObject] parent: parent in the nexus hierarchy
+        :param node_name: name of the detector in the hierarchy
+        :param parent: parent in the nexus hierarchy
         """
         super().__init__(node_name=node_name, parent=parent)
         self._set_freeze(False)
@@ -51,44 +52,44 @@ class NXinstrument(NXobject):
         self._set_freeze(True)
 
     @property
-    def detector(self) -> Optional[NXdetector]:
+    def detector(self) -> NXdetector | None:
         """
         :class:`~nxtomo.nxobject.nxdetector.NXdetector`
         """
         return self._detector
 
     @detector.setter
-    def detector(self, detector: Optional[NXdetector]):
+    def detector(self, detector: NXdetector | None):
         if not isinstance(detector, (NXdetector, type(None))):
             raise TypeError(
-                f"detector is expected to be None or an instance of NXdetecetor. Not {type(detector)}"
+                f"detector is expected to be None or an instance of NXdetector. Not {type(detector)}"
             )
         self._detector = detector
 
     @property
-    def diode(self) -> Optional[NXdetector]:
+    def diode(self) -> NXdetector | None:
         """
         :class:`~nxtomo.nxobject.nxdetector.NXdetector`
         """
         return self._diode
 
     @diode.setter
-    def diode(self, diode: Optional[NXdetector]):
+    def diode(self, diode: NXdetector | None):
         if not isinstance(diode, (NXdetector, type(None))):
             raise TypeError(
-                f"diode is expected to be None or an instance of NXdetecetor. Not {type(diode)}"
+                f"diode is expected to be None or an instance of NXdetector. Not {type(diode)}"
             )
         self._diode = diode
 
     @property
-    def source(self) -> Optional[NXsource]:
+    def source(self) -> NXsource | None:
         """
         :class:`~nxtomo.nxobject.nxdetector.NXsource`
         """
         return self._source
 
     @source.setter
-    def source(self, source: Optional[NXsource]) -> None:
+    def source(self, source: NXsource | None) -> None:
         if not isinstance(source, (NXsource, type(None))):
             raise TypeError(
                 f"source is expected to be None or an instance of NXsource. Not {type(source)}"
@@ -96,12 +97,12 @@ class NXinstrument(NXobject):
         self._source = source
 
     @property
-    def name(self) -> Optional[str]:
+    def name(self) -> str | None:
         """instrument name like BM00"""
         return self._name
 
     @name.setter
-    def name(self, name: Optional[str]) -> None:
+    def name(self, name: str | None) -> None:
         if not isinstance(name, (str, type(None))):
             raise TypeError(
                 f"name is expected to be None or an instance of str. Not {type(name)}"
@@ -111,8 +112,8 @@ class NXinstrument(NXobject):
     @docstring(NXobject)
     def to_nx_dict(
         self,
-        nexus_path_version: Optional[float] = None,
-        data_path: Optional[str] = None,
+        nexus_path_version: float | None = None,
+        data_path: str | None = None,
     ) -> dict:
         nexus_paths = get_nexus_paths(nexus_path_version)
         nexus_instrument_paths = nexus_paths.nx_instrument_paths
diff --git a/nxtomo/nxobject/nxmonitor.py b/nxtomo/nxobject/nxmonitor.py
index 102a783c9105a03205264cd3fef00ba150e8d6be..1d4bfb39e5138e1555903293aa0206e1d8b0ae99 100644
--- a/nxtomo/nxobject/nxmonitor.py
+++ b/nxtomo/nxobject/nxmonitor.py
@@ -2,9 +2,10 @@
 module for handling a `nxmonitor <https://manual.nexusformat.org/classes/base_classes/NXmonitor.html>`_
 """
 
+from __future__ import annotations
+
 from functools import partial
 from operator import is_not
-from typing import Optional, Union
 import numpy
 
 from silx.utils.proxy import docstring
@@ -17,13 +18,13 @@ from pyunitsystem import ElectricCurrentSystem
 
 
 class NXmonitor(NXobject):
-    def __init__(self, node_name="control", parent: Optional[NXobject] = None) -> None:
+    def __init__(self, node_name="control", parent: NXobject | None = None) -> None:
         """
         representation of `nexus NXmonitor <https://manual.nexusformat.org/classes/base_classes/NXmonitor.html>`_.
         A monitor of incident beam data.
 
-        :param str node_name: name of the detector in the hierarchy
-        :param Optional[NXObject] parent: parent in the nexus hierarchy
+        :param node_name: name of the detector in the hierarchy
+        :param parent: parent in the nexus hierarchy
         """
         super().__init__(node_name=node_name, parent=parent)
         self._set_freeze(False)
@@ -31,7 +32,7 @@ class NXmonitor(NXobject):
         self._set_freeze(True)
 
     @property
-    def data(self) -> Optional[numpy.ndarray]:
+    def data(self) -> numpy.ndarray | None:
         """
         monitor data.
         In the case of NXtomo it expects to contains machine electric current for each frame
@@ -39,7 +40,7 @@ class NXmonitor(NXobject):
         return self._data
 
     @data.setter
-    def data(self, data: Optional[Union[numpy.ndarray, list, tuple]]):
+    def data(self, data: numpy.ndarray | list | tuple | None):
         if isinstance(data, (tuple, list)):
             if len(data) == 0:
                 data = None
@@ -58,8 +59,8 @@ class NXmonitor(NXobject):
     @docstring(NXobject)
     def to_nx_dict(
         self,
-        nexus_path_version: Optional[float] = None,
-        data_path: Optional[str] = None,
+        nexus_path_version: float | None = None,
+        data_path: str | None = None,
     ) -> dict:
         nexus_paths = get_nexus_paths(nexus_path_version)
         monitor_nexus_paths = nexus_paths.nx_monitor_paths
diff --git a/nxtomo/nxobject/nxobject.py b/nxtomo/nxobject/nxobject.py
index 93d7388be0c245b19193502c4b8376bbfdf3976f..cd8c0cb1f3ff399fd013857028cd89ea5e407d76 100644
--- a/nxtomo/nxobject/nxobject.py
+++ b/nxtomo/nxobject/nxobject.py
@@ -2,9 +2,10 @@
 module for handling a `nxobject <https://manual.nexusformat.org/classes/base_classes/NXobject.html>`_
 """
 
+from __future__ import annotations
+
 import os
 import logging
-from typing import Optional
 
 import h5py
 from silx.io.dictdump import dicttonx
@@ -27,7 +28,7 @@ class ElementWithUnit:
         """
         Util class to let the user define a unit with a value
 
-        :param Unit default_unit: default unit of the element
+        :param default_unit: default unit of the element
         """
 
         if not isinstance(default_unit, Unit):
@@ -37,7 +38,7 @@ class ElementWithUnit:
         self._unit_type = type(default_unit)
 
     @property
-    def unit(self) -> Optional[float]:
+    def unit(self) -> float | None:
         """
         unit as a float to cast it to SI
         """
@@ -91,14 +92,14 @@ class NXobject:
         A monitor of incident beam data.
 
         :param node_name: name of the detector in the hierarchy
-        :param Optional[NXObject] parent: parent in the nexus hierarchy
+        :param parent: parent in the nexus hierarchy
         """
         if not isinstance(node_name, str):
             raise TypeError(
                 f"name is expected to be an instance of str. Not {type(node_name)}"
             )
         if "/" in node_name:
-            # make sure there is no '/' character. This is reserved to define the NXobject hierachy
+            # make sure there is no '/' character. This is reserved to define the NXobject hierarchy
             raise ValueError(
                 "'/' found in 'node_name' parameter. This is a reserved character. Please change the name"
             )
@@ -110,7 +111,7 @@ class NXobject:
         self.__isfrozen = freeze
 
     @property
-    def parent(self):  # -> Optional[NXobject]:
+    def parent(self):  # -> NXobject | None:
         """
         :class:`~nxtomo.nxobject.nxobject.NXobject` parent in the hierarchy
         """
@@ -167,17 +168,17 @@ class NXobject:
     def save(
         self,
         file_path: str,
-        data_path: Optional[str] = None,
-        nexus_path_version: Optional[float] = None,
+        data_path: str | None = None,
+        nexus_path_version: float | None = None,
         overwrite: bool = False,
     ) -> None:
         """
         save NXtomo to disk.
 
-        :param str file_path: hdf5 file
-        :param str data_path: location to the NXobject. If not provided will be stored under node_name if provided (and valid)
+        :param file_path: hdf5 file
+        :param data_path: location to the NXobject. If not provided will be stored under node_name if provided (and valid)
         :param nexus_path_version: Optional nexus version as float. If the saving must be done **not** using the latest version
-        :param bool overwrite: if the data_path in file_path is already existing overwrite it. Else raise will raise an error
+        :param overwrite: if the data_path in file_path is already existing overwrite it. Else raise will raise an error
         """
         if data_path == "/":
             _logger.warning(
@@ -324,14 +325,14 @@ class NXobject:
 
     def to_nx_dict(
         self,
-        nexus_path_version: Optional[float] = None,
-        data_path: Optional[str] = None,
+        nexus_path_version: float | None = None,
+        data_path: str | None = None,
     ) -> dict:
         """
-        convert the NXobject to an nx dict. Dictionnary that we can dump to hdf5 file
+        convert the NXobject to an nx dict. Dictionary that we can dump to hdf5 file
 
-        :param Optional[float] nexus_path_version: version of the nexus path version to use
-        :param Optional[str] data_path: can be provided to create some link in the file
+        :param nexus_path_version: version of the nexus path version to use
+        :param data_path: can be provided to create some link in the file
         """
         raise NotImplementedError("Base class")
 
@@ -395,6 +396,6 @@ class NXobject:
         """
         concatenate a tuple of NXobject into a single NXobject
         :param Iterable Nx-objects: nx object to concatenate
-        :param str node_name: name of the node to create. Parent must be handled manually for now.
+        :param node_name: name of the node to create. Parent must be handled manually for now.
         """
         raise NotImplementedError("Base class")
diff --git a/nxtomo/nxobject/nxsample.py b/nxtomo/nxobject/nxsample.py
index c7d33953cae9ee5c365138c3ba7b15c0ded6f22d..1ba6a452f0da760e5b5a8a80a7cf0db46a5f9416 100644
--- a/nxtomo/nxobject/nxsample.py
+++ b/nxtomo/nxobject/nxsample.py
@@ -2,10 +2,12 @@
 module for handling a `nxsample <https://manual.nexusformat.org/classes/base_classes/NXsample.html>`_
 """
 
+from __future__ import annotations
+
 import logging
 from functools import partial
 from operator import is_not
-from typing import Iterable, Optional, Tuple
+from typing import Iterable
 
 import numpy
 from silx.utils.proxy import docstring
@@ -20,13 +22,13 @@ _logger = logging.getLogger(__name__)
 
 
 class NXsample(NXobject):
-    def __init__(self, node_name="sample", parent: Optional[NXobject] = None) -> None:
+    def __init__(self, node_name="sample", parent: NXobject | None = None) -> None:
         """
         representation of `nexus NXsample <https://manual.nexusformat.org/classes/base_classes/NXsample.html>`_.
         A monitor of incident beam data.
 
-        :param str node_name: name of the detector in the hierarchy
-        :param Optional[NXObject] parent: parent in the nexus hierarchy
+        :param node_name: name of the detector in the hierarchy
+        :param parent: parent in the nexus hierarchy
         """
         super().__init__(node_name=node_name, parent=parent)
         self._set_freeze(False)
@@ -42,65 +44,65 @@ class NXsample(NXobject):
         self._set_freeze(True)
 
     @property
-    def name(self) -> Optional[str]:
+    def name(self) -> str | None:
         """sample name"""
         return self._name
 
     @name.setter
-    def name(self, name: Optional[str]) -> None:
+    def name(self, name: str | None) -> None:
         if not isinstance(name, (type(None), str)):
             raise TypeError(f"name is expected to be None or str not {type(name)}")
         self._name = name
 
     @property
-    def rotation_angle(self) -> Optional[numpy.ndarray]:
+    def rotation_angle(self) -> numpy.ndarray | None:
         """sample rotation angle. One per frame"""
         return self._rotation_angle
 
     @rotation_angle.setter
-    def rotation_angle(self, rotation_angle: Optional[Iterable]):
+    def rotation_angle(self, rotation_angle: Iterable | None):
         self._rotation_angle = cast_and_check_array_1D(rotation_angle, "rotation_angle")
 
     @property
-    def x_translation(self) -> Optional[numpy.ndarray]:
-        """sample translation along x. See `modelization at esrf <https://tomo.gitlab-pages.esrf.fr/ebs-tomo/master/modelization.html>`_ for more information"""
+    def x_translation(self) -> numpy.ndarray | None:
+        """sample translation along x. See `modelling at esrf <https://tomo.gitlab-pages.esrf.fr/ebs-tomo/master/modelization.html>`_ for more information"""
         return self._x_translation
 
     @x_translation.setter
-    def x_translation(self, x_translation: Optional[Iterable]):
+    def x_translation(self, x_translation: Iterable | None):
         self._x_translation.value = cast_and_check_array_1D(
             x_translation, "x_translation"
         )
 
     @property
-    def y_translation(self) -> Optional[numpy.ndarray]:
-        """sample translation along y. See `modelization at esrf <https://tomo.gitlab-pages.esrf.fr/ebs-tomo/master/modelization.html>`_ for more information"""
+    def y_translation(self) -> numpy.ndarray | None:
+        """sample translation along y. See `modelling at esrf <https://tomo.gitlab-pages.esrf.fr/ebs-tomo/master/modelization.html>`_ for more information"""
         return self._y_translation
 
     @y_translation.setter
-    def y_translation(self, y_translation: Optional[Iterable]):
+    def y_translation(self, y_translation: Iterable | None):
         self._y_translation.value = cast_and_check_array_1D(
             y_translation, "y_translation"
         )
 
     @property
-    def z_translation(self) -> Optional[numpy.ndarray]:
-        """sample translation along z. See `modelization at esrf <https://tomo.gitlab-pages.esrf.fr/ebs-tomo/master/modelization.html>`_ for more information"""
+    def z_translation(self) -> numpy.ndarray | None:
+        """sample translation along z. See `modelling at esrf <https://tomo.gitlab-pages.esrf.fr/ebs-tomo/master/modelization.html>`_ for more information"""
         return self._z_translation
 
     @z_translation.setter
-    def z_translation(self, z_translation: Optional[Iterable]):
+    def z_translation(self, z_translation: Iterable | None):
         self._z_translation.value = cast_and_check_array_1D(
             z_translation, "z_translation"
         )
 
     @property
-    def transformations(self) -> Tuple[NXtransformations]:
+    def transformations(self) -> tuple[NXtransformations]:
         """detector transformations as `NXtransformations <https://manual.nexusformat.org/classes/base_classes/NXtransformations.html>`_"""
         return self._transformations
 
     @transformations.setter
-    def transformations(self, transformations: Tuple[NXtransformations]):
+    def transformations(self, transformations: tuple[NXtransformations]):
         if not isinstance(transformations, tuple):
             raise TypeError
         for transformation in transformations:
@@ -110,8 +112,8 @@ class NXsample(NXobject):
     @docstring(NXobject)
     def to_nx_dict(
         self,
-        nexus_path_version: Optional[float] = None,
-        data_path: Optional[str] = None,
+        nexus_path_version: float | None = None,
+        data_path: str | None = None,
     ) -> dict:
         nexus_paths = get_nexus_paths(nexus_path_version)
         nexus_sample_paths = nexus_paths.nx_sample_paths
diff --git a/nxtomo/nxobject/nxsource.py b/nxtomo/nxobject/nxsource.py
index baad36acc39a2a4a8a1458ca27570baf7c289f7a..0a23c8e440aa10e3e0001adb8515f0358c2265ce 100644
--- a/nxtomo/nxobject/nxsource.py
+++ b/nxtomo/nxobject/nxsource.py
@@ -2,10 +2,11 @@
 module for handling a `nxsource <https://manual.nexusformat.org/classes/base_classes/NXsource.html>`_
 """
 
+from __future__ import annotations
+
 import logging
 from functools import partial
 from operator import is_not
-from typing import Optional, Union
 
 import numpy
 from silx.utils.enum import Enum as _Enum
@@ -82,14 +83,14 @@ class NXsource(NXobject):
         self._set_freeze(True)
 
     @property
-    def name(self) -> Union[None, str]:
+    def name(self) -> None | str:
         """
         source name
         """
         return self._name
 
     @name.setter
-    def name(self, source_name: Union[str, None]):
+    def name(self, source_name: str | None):
         if isinstance(source_name, numpy.ndarray):
             # handle Diamond Dataset
             source_name = source_name.tostring()
@@ -102,14 +103,14 @@ class NXsource(NXobject):
         self._name = source_name
 
     @property
-    def type(self) -> Optional[SourceType]:
+    def type(self) -> SourceType | None:
         """
         source type as :class:`~nxtomo.nxobject.nxsource.SourceType`
         """
         return self._type
 
     @type.setter
-    def type(self, type_: Union[None, str, SourceType]):
+    def type(self, type_: None | str | SourceType):
         if type_ is None:
             self._type = None
         else:
@@ -117,14 +118,14 @@ class NXsource(NXobject):
             self._type = type_
 
     @property
-    def probe(self) -> Optional[ProbeType]:
+    def probe(self) -> ProbeType | None:
         """
         probe as :class:`~nxtomo.nxobject.nxsource.ProbeType`
         """
         return self._probe
 
     @probe.setter
-    def probe(self, probe: Union[None, str, ProbeType]):
+    def probe(self, probe: None | str | ProbeType):
         if probe is None:
             self._probe = None
         else:
@@ -136,8 +137,8 @@ class NXsource(NXobject):
     @docstring(NXobject)
     def to_nx_dict(
         self,
-        nexus_path_version: Optional[float] = None,
-        data_path: Optional[str] = None,
+        nexus_path_version: float | None = None,
+        data_path: str | None = None,
     ) -> dict:
         nexus_paths = get_nexus_paths(nexus_path_version)
         nexus_source_paths = nexus_paths.nx_source_paths
diff --git a/nxtomo/nxobject/nxtransformations.py b/nxtomo/nxobject/nxtransformations.py
index 123b750097200bb3c3739ec7efe387efc914bbae..337094995f18ffe02ecceb4e54cf610d373868e2 100644
--- a/nxtomo/nxobject/nxtransformations.py
+++ b/nxtomo/nxobject/nxtransformations.py
@@ -2,7 +2,8 @@
 module for handling a `nxtransformations <https://manual.nexusformat.org/classes/base_classes/nxtransformations.html#nxtransformations>`_
 """
 
-import typing
+from __future__ import annotations
+
 import logging
 import h5py
 
@@ -31,13 +32,13 @@ class NXtransformations(NXobject):
 
         For tomotools the first usage would be to allow users to provide more metadata to tag acquisition (like 'detector has been rotate' of 90 degree...)
 
-        :param str node_name: name of the detector in the hierarchy
-        :param Optional[NXObject] parent: parent in the nexus hierarchy
+        :param node_name: name of the detector in the hierarchy
+        :param parent: parent in the nexus hierarchy
         """
         super().__init__(node_name, parent)
         self._set_freeze(False)
         self._transformations = dict()
-        # dict with axis_name as value and Transforamtion as value. Simplify handling compared to a tuple / list / set and ensure the axis_name is unique
+        # dict with axis_name as value and Transformation as value. Simplify handling compared to a tuple / list / set and ensure the axis_name is unique
         self._set_freeze(True)
 
     @property
@@ -50,7 +51,7 @@ class NXtransformations(NXobject):
     @transformations.setter
     def transformations(self, transformations: tuple):
         """
-        :param dict transformations: dict as [str, Transformation]
+        :param transformations: dict as [str, Transformation]
         """
         # check type
         if not isinstance(transformations, (tuple, list)):
@@ -62,7 +63,7 @@ class NXtransformations(NXobject):
                 raise TypeError(
                     f"element are expected to be instances of {Transformation}. {type(transformation)} provided instead"
                 )
-        # convert it to a dict for conveniance
+        # convert it to a dict for convenience
         self._transformations = {
             transformation.axis_name: transformation
             for transformation in transformations
@@ -78,10 +79,10 @@ class NXtransformations(NXobject):
         """
         add a transformation to the existing one.
 
-        :param Transformation transformation: transformation to be added
-        :param bool overwrite: if a transformation with the same axis_name already exists then overwrite it
-        :param bool skip_if_exists: if a transformation with the same axis_name already exists then keep the existing one
-        :raises: KeyError, if a transforamtion with the same axis_name already registered
+        :param transformation: transformation to be added
+        :param overwrite: if a transformation with the same axis_name already exists then overwrite it
+        :param skip_if_exists: if a transformation with the same axis_name already exists then keep the existing one
+        :raises: KeyError, if a transformation with the same axis_name already registered
         """
         if skip_if_exists is overwrite is True:
             raise ValueError(
@@ -119,12 +120,12 @@ class NXtransformations(NXobject):
     @docstring(NXobject)
     def to_nx_dict(
         self,
-        nexus_path_version: typing.Optional[float] = None,
-        data_path: typing.Optional[str] = None,
+        nexus_path_version: float | None = None,
+        data_path: str | None = None,
         solve_empty_dependency: bool = False,
     ) -> dict:
         """
-        :param bool append_gravity: If True all transformation without dependancy will be depending on a "gravity" Transformation which represent the gravity
+        :param append_gravity: If True all transformation without dependency will be depending on a "gravity" Transformation which represent the gravity
         """
         if len(self._transformations) == 0:
             # if no transformation, avoid creating the group
@@ -177,9 +178,7 @@ class NXtransformations(NXobject):
         return nx_dict
 
     @staticmethod
-    def load_from_file(
-        file_path: str, data_path: str, nexus_version: typing.Optional[float]
-    ):
+    def load_from_file(file_path: str, data_path: str, nexus_version: float | None):
         """
         create an instance of :class:`~nxtomo.nxobject.nxtransformations,NXtransformations` and load it value from
         the given file and data path
@@ -190,7 +189,7 @@ class NXtransformations(NXobject):
         )
 
     def _load(
-        self, file_path: str, data_path: str, nexus_version: typing.Optional[float]
+        self, file_path: str, data_path: str, nexus_version: float | None
     ) -> NXobject:
         """
         Create and load an NXmonitor from data on disk
@@ -268,7 +267,7 @@ class NXtransformations(NXobject):
         return len(self.transformations)
 
 
-def get_lr_flip(transformations: typing.Union[tuple, NXtransformations]) -> tuple:
+def get_lr_flip(transformations: tuple | NXtransformations) -> tuple:
     """
     check along all transformations if find Transformation matching 'LRTransformation'
 
@@ -279,7 +278,7 @@ def get_lr_flip(transformations: typing.Union[tuple, NXtransformations]) -> tupl
     return _get_lr_flip(transformations)
 
 
-def get_ud_flip(transformations: typing.Union[tuple, NXtransformations]) -> tuple:
+def get_ud_flip(transformations: tuple | NXtransformations) -> tuple:
     """
     check along all transformations if find Transformation matching 'UDTransformation'
 
diff --git a/nxtomo/nxobject/test/test_nxobject.py b/nxtomo/nxobject/test/test_nxobject.py
index 0a99509f1c9c45275aea028406a3963cfce02948..75687ceb7cdcf0f6ad554e7742c7a0e045cac20b 100644
--- a/nxtomo/nxobject/test/test_nxobject.py
+++ b/nxtomo/nxobject/test/test_nxobject.py
@@ -1,6 +1,6 @@
+from __future__ import annotations
 import os
 from tempfile import TemporaryDirectory
-from typing import Optional
 
 import numpy
 import pytest
@@ -31,8 +31,8 @@ class test_nx_object:
     class MyNXObject(NXobject):
         def to_nx_dict(
             self,
-            nexus_path_version: Optional[float] = None,
-            data_path: Optional[str] = None,
+            nexus_path_version: float | None = None,
+            data_path: str | None = None,
         ) -> dict:
             return {
                 f"{self.path}/test": "toto",
diff --git a/nxtomo/paths/nxtomo.py b/nxtomo/paths/nxtomo.py
index 0e5cdcd1baee57b408ea0c7724e800696ca57daf..7dec28b92ee171ebbdfeab928be57ee5658a0a0d 100644
--- a/nxtomo/paths/nxtomo.py
+++ b/nxtomo/paths/nxtomo.py
@@ -1,7 +1,8 @@
 """nexus path used to define a `NXtomo <https://manual.nexusformat.org/classes/base_classes/NXtomo.html>`_"""
 
+from __future__ import annotations
+
 import logging
-from typing import Optional
 
 from silx.utils.deprecation import deprecated
 
@@ -289,19 +290,19 @@ class NXtomo_PATH:
         return 0.02
 
     @property
-    def SOURCE_NAME(self) -> Optional[str]:
+    def SOURCE_NAME(self) -> str | None:
         return None
 
     @property
-    def SOURCE_TYPE(self) -> Optional[str]:
+    def SOURCE_TYPE(self) -> str | None:
         return None
 
     @property
-    def SOURCE_PROBE(self) -> Optional[str]:
+    def SOURCE_PROBE(self) -> str | None:
         return None
 
     @property
-    def INSTRUMENT_NAME(self) -> Optional[str]:
+    def INSTRUMENT_NAME(self) -> str | None:
         return None
 
     @property
@@ -417,7 +418,7 @@ nx_tomo_path_v_1_3 = NXtomo_PATH_v_1_3()
 nx_tomo_path_latest = nx_tomo_path_v_1_3
 
 
-def get_paths(version: Optional[float]) -> NXtomo_PATH:
+def get_paths(version: float | None) -> NXtomo_PATH:
     if version is None:
         version = LATEST_VERSION
         _logger.warning(
diff --git a/nxtomo/utils/detectorsplitter.py b/nxtomo/utils/detectorsplitter.py
index c427e5bd4fd3ec3bd6ed0f0e1909a541e92770b1..b8e7e39d6a5e8b9d9d55457a239f2b5d5421710d 100644
--- a/nxtomo/utils/detectorsplitter.py
+++ b/nxtomo/utils/detectorsplitter.py
@@ -2,9 +2,10 @@
 module to split a NXtomo into several
 """
 
+from __future__ import annotations
+
 import copy
 import logging
-from typing import Optional, Union
 
 import h5py
 import h5py._hl.selections as selection
@@ -28,7 +29,7 @@ class NXtomoDetectorDataSplitter:
         In order to start the processing it requires a correctly formed NXtomo (same number of image_key, rotation_angle...)
         This is required for the pcotomo acquisition.
 
-        :param :class:`~nxtomo.nxobject.nxobject.NXobject` nx_tomo: nx_tomo to be splitted
+        :param nx_tomo: nx_tomo to be splitted
         """
         if not isinstance(nx_tomo, NXtomo):
             raise TypeError(
@@ -41,12 +42,12 @@ class NXtomoDetectorDataSplitter:
         """
         nx_tomo to be splitted
 
-        :param :class:`~nxtomo.nxobject.nxobject.NXobject` nx_tomo: nx_tomo to be splitted
+        :param nx_tomo: nx_tomo to be splitted
         """
         return self._nx_tomo
 
     def split(
-        self, data_slice: slice, nb_part: Optional[int], tomo_n: Optional[int] = None
+        self, data_slice: slice, nb_part: int | None, tomo_n: int | None = None
     ) -> tuple:
         """
         split the dataset targetted to have a set of h5py.VirtualSource.
@@ -55,8 +56,8 @@ class NXtomoDetectorDataSplitter:
         request seems incoherent according to the number of projection and tomo_n then it will fall back on using
         tomo_n for it
 
-        :param int nb_part: in how many contiguous dataset the instruement.detector.data must be splitted.
-        :param int tomo_n: expected number of projection per NXtomo
+        :param nb_part: in how many contiguous dataset the instrument.detector.data must be splitted.
+        :param tomo_n: expected number of projection per NXtomo
         :raises: ValueError if the number of frame, image_key, x_translation... is incoherent.
         """
         if nb_part is not None and not isinstance(
@@ -267,7 +268,7 @@ class NXtomoDetectorDataSplitter:
         if section.start == section.stop:
             return ()
 
-        def get_elmt_shape(elmt: Union[h5py.VirtualSource, DataUrl]) -> tuple:
+        def get_elmt_shape(elmt: h5py.VirtualSource | DataUrl) -> tuple:
             if isinstance(elmt, h5py.VirtualSource):
                 return elmt.shape
             elif isinstance(elmt, DataUrl):
@@ -278,7 +279,7 @@ class NXtomoDetectorDataSplitter:
                     f"elmt must be a DataUrl or h5py.VirtualSource. Not {type(elmt)}"
                 )
 
-        def get_elmt_nb_frame(elmt: Union[h5py.VirtualSource, DataUrl]) -> int:
+        def get_elmt_nb_frame(elmt: h5py.VirtualSource | DataUrl) -> int:
             shape = get_elmt_shape(elmt)
             if len(shape) == 3:
                 return shape[0]
@@ -306,8 +307,8 @@ class NXtomoDetectorDataSplitter:
             return slice_1.start < slice_2.stop and slice_1.stop > slice_2.start
 
         def select(
-            elmt: Union[h5py.VirtualSource, DataUrl], region: slice
-        ) -> Union[h5py.VirtualSource, DataUrl]:
+            elmt: h5py.VirtualSource | DataUrl, region: slice
+        ) -> h5py.VirtualSource | DataUrl:
             """select a region on the elmt.
             Can return at most the elmt itself or a region of it"""
             elmt_n_frame = get_elmt_nb_frame(elmt)
@@ -420,7 +421,7 @@ class NXtomoDetectorDataSplitter:
 
         return invalid_datasets
 
-    def _get_n_frames(self) -> Optional[int]:
+    def _get_n_frames(self) -> int | None:
         dataset = self.nx_tomo.instrument.detector.data
         if dataset is None:
             return None
diff --git a/nxtomo/utils/frameappender.py b/nxtomo/utils/frameappender.py
index 50b17652c2c1a050829b67057d3563edf876fcdb..d2e24c2b81a84915f7c665ece859c3ecfb833d6d 100644
--- a/nxtomo/utils/frameappender.py
+++ b/nxtomo/utils/frameappender.py
@@ -2,8 +2,9 @@
 module to append frame to an hdf5 dataset (that can be virtual)
 """
 
+from __future__ import annotations
+
 import os
-from typing import Union
 
 import h5py
 import h5py._hl.selections as selection
@@ -24,7 +25,7 @@ from nxtomo.utils.io import DatasetReader
 class FrameAppender:
     def __init__(
         self,
-        data: Union[numpy.ndarray, DataUrl],
+        data: numpy.ndarray | DataUrl,
         file_path: str,
         data_path: str,
         where: str,
@@ -34,10 +35,10 @@ class FrameAppender:
         Class to insert 2D frame(s) to an existing dataset
 
         :param data: data to append
-        :param str file_path: file path of the HDF5 dataset to extend
-        :param str data_path: file data_path of the HDF5 dataset to extend
-        :param str where: can be 'start' or 'end' to know if we should append frame at the beginning or at the end
-        :param logger: optioanl logger to handle logs
+        :param file_path: file path of the HDF5 dataset to extend
+        :param data_path: file data_path of the HDF5 dataset to extend
+        :param where: can be 'start' or 'end' to know if we should append frame at the beginning or at the end
+        :param logger: optional logger to handle logs
         """
         if where not in ("start", "end"):
             raise ValueError("`where` should be `start` or `end`")
diff --git a/nxtomo/utils/io.py b/nxtomo/utils/io.py
index 5fbcf4a4dccb3a7c4185dc2ec41bc364373513a3..e58542876322d94c88ad28b6e16fe47a2027d491 100644
--- a/nxtomo/utils/io.py
+++ b/nxtomo/utils/io.py
@@ -80,18 +80,18 @@ def deprecated_warning(
     """
     Function to log a deprecation warning
 
-    :param str type_: Nature of the object to be deprecated:
+    :param type_: Nature of the object to be deprecated:
         "Module", "Function", "Class" ...
     :param name: Object name.
-    :param str reason: Reason for deprecating this function
+    :param reason: Reason for deprecating this function
         (e.g. "feature no longer provided",
-    :param str replacement: Name of replacement function (if the reason for
+    :param replacement: Name of replacement function (if the reason for
         deprecating was to rename the function)
-    :param str since_version: First *silx* version for which the function was
+    :param since_version: First *silx* version for which the function was
         deprecated (e.g. "0.5.0").
-    :param bool only_once: If true, the deprecation warning will only be
+    :param only_once: If true, the deprecation warning will only be
         generated one time for each different call locations. Default is true.
-    :param int skip_backtrace_count: Amount of last backtrace to ignore when
+    :param skip_backtrace_count: Amount of last backtrace to ignore when
         logging the backtrace
     """
     if not depreclog.isEnabledFor(logging.WARNING):
diff --git a/nxtomo/utils/transformation.py b/nxtomo/utils/transformation.py
index 415016c6ec28c5dac63ef0c16de6dca87c6af7ec..740a329618ebe7679467e7298e3ebcc4b260724d 100644
--- a/nxtomo/utils/transformation.py
+++ b/nxtomo/utils/transformation.py
@@ -1,6 +1,7 @@
 """module to provide helper classes to define transformations contained in NXtransformations"""
 
-import typing
+from __future__ import annotations
+
 import logging
 import numpy
 
@@ -39,10 +40,10 @@ class Transformation:
     """
     Define a Transformation done on an axis
 
-    :param str axis_name: name of the transformation.
-    :param TransformationType transformation_type: type of the formation. As unit depends on the type of transformation this is not possible to modify it once created
+    :param axis_name: name of the transformation.
+    :param transformation_type: type of the formation. As unit depends on the type of transformation this is not possible to modify it once created
     :param vector: vector of the transformation. Expected as a tuple of three values that define the axis for this transformation. Can also be an instance of TransformationAxis predefining some default axis
-    :param Optional[str] depends_on: used to determine transformation chain. If depends on no other transformation then should be considered as if it is depending on "gravity" only.
+    :param depends_on: used to determine transformation chain. If depends on no other transformation then should be considered as if it is depending on "gravity" only.
     :warning: when convert a rotation which as 'radian' as units it will be cast to degree
     """
 
@@ -55,8 +56,8 @@ class Transformation:
         axis_name: str,
         value,
         transformation_type: TransformationType,
-        vector: typing.Union[typing.Tuple[float, float, float], TransformationAxis],
-        depends_on: typing.Optional[str] = None,
+        vector: tuple[float, float, float] | TransformationAxis,
+        depends_on: str | None = None,
     ) -> None:
         self._axis_name = axis_name
         self._transformation_values = None
@@ -113,7 +114,7 @@ class Transformation:
         return self._units
 
     @units.setter
-    def units(self, units: typing.Union[str, MetricSystem]):
+    def units(self, units: str | MetricSystem):
         """
         :raises ValueError: if units is invalid (depends on the transformation type).
         """
@@ -130,7 +131,7 @@ class Transformation:
             raise ValueError(f"Unrecognized unit {units}")
 
     @property
-    def vector(self) -> typing.Tuple[float, float, float]:
+    def vector(self) -> tuple[float, float, float]:
         return self._vector
 
     @property
@@ -138,7 +139,7 @@ class Transformation:
         return self._offset
 
     @offset.setter
-    def offset(self, offset: typing.Union[tuple, list, numpy.ndarray]):
+    def offset(self, offset: tuple | list | numpy.ndarray):
         if not isinstance(offset, (tuple, list, numpy.ndarray)):
             raise TypeError(
                 f"offset is expected to be a vector of three elements. {type(offset)} provided"
@@ -156,7 +157,7 @@ class Transformation:
     @depends_on.setter
     def depends_on(self, depends_on):
         """
-        :param Optional[Transformation] depends_on:
+        :param  depends_on:
         """
         if not (depends_on is None or isinstance(depends_on, str)):
             raise TypeError(
@@ -165,11 +166,11 @@ class Transformation:
         self._depends_on = depends_on
 
     @property
-    def equipment_component(self) -> typing.Optional[str]:
+    def equipment_component(self) -> str | None:
         return self._equipment_component
 
     @equipment_component.setter
-    def equipment_component(self, equipment_component: typing.Optional[str]):
+    def equipment_component(self, equipment_component: str | None):
         if not (equipment_component is None or isinstance(equipment_component, str)):
             raise TypeError(
                 f"equipment_component is expect to ne None or a str. {type(equipment_component)} provided"
diff --git a/nxtomo/utils/utils.py b/nxtomo/utils/utils.py
index 5860fe49a23284d187a282e405b1783a4ad7d949..a980707bad997bbfcc4937157662929847831c83 100644
--- a/nxtomo/utils/utils.py
+++ b/nxtomo/utils/utils.py
@@ -1,5 +1,7 @@
 """general utils"""
 
+from __future__ import annotations
+
 from typing import Iterable
 import h5py
 import numpy
@@ -12,7 +14,7 @@ def cast_and_check_array_1D(array, array_name: str):
     cast provided array to 1D
 
     :param array: array to be cast to 1D
-    :param str array_name: name of the array - used for log only
+    :param array_name: name of the array - used for log only
     """
     if not isinstance(array, (type(None), numpy.ndarray, Iterable)):
         raise TypeError(
@@ -29,8 +31,8 @@ def get_data_and_unit(file_path: str, data_path: str, default_unit):
     """
     return for an HDF5 dataset his value and his unit. If unit cannot be found then fallback on the 'default_unit'
 
-    :param str file_path: file path location of the HDF5Dataset to read
-    :param str data_path: data_path location of the HDF5Dataset to read
+    :param file_path: file path location of the HDF5Dataset to read
+    :param data_path: data_path location of the HDF5Dataset to read
     :param default_unit: default unit to fall back if the dataset has no 'unit' or 'units' attribute
     """
     with hdf5_open(file_path) as h5f:
@@ -56,8 +58,8 @@ def get_data(file_path: str, data_path: str):
     proxy to h5py_read_dataset, handling use case 'data_path' not present in the file.
     In this case return None
 
-    :param str file_path: file path location of the HDF5Dataset to read
-    :param str data_path: data_path location of the HDF5Dataset to read
+    :param file_path: file path location of the HDF5Dataset to read
+    :param data_path: data_path location of the HDF5Dataset to read
     """
     with hdf5_open(file_path) as h5f:
         if data_path in h5f: