Repository: larray-project/larray (language: python)

Issue:
provide long_description_content_type in setup.py of all projects
During release we get this warning:

twine check dist/*
Checking dist\larray_eurostat-0.34-py3-none-any.whl: PASSED, with warnings
  warning: `long_description_content_type` missing. defaulting to `text/x-rst`.
Checking dist\larray_eurostat-0.34.tar.gz: PASSED, with warnings
  warning: `long_description_content_type` missing. defaulting to `text/x-rst`.
deprecate Axis.subaxis (make it a private method)



Discussion / hints:

Fix patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 499e30588..25e67559c 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -33,7 +33,7 @@ jobs:
       fail-fast: false
       matrix:
         # os: ["ubuntu-latest", "macos-latest", "windows-latest"]
-        python-version: ['3.7', '3.8', '3.9', '3.10']
+        python-version: ['3.8', '3.9', '3.10', '3.11']
 
     defaults:
       run:
diff --git a/README.rst b/README.rst
index 3e9d2d6c1..cc1cd3fbe 100644
--- a/README.rst
+++ b/README.rst
@@ -73,8 +73,8 @@ Once you have satisfied the requirements detailed below, simply run::
 Required Dependencies
 ---------------------
 
-- Python 3.7, 3.8, 3.9 or 3.10
-- `numpy <http://www.numpy.org/>`__ (1.13 or later)
+- Python 3.8, 3.9, 3.10 or 3.11
+- `numpy <http://www.numpy.org/>`__ (1.22 or later)
 - `pandas <http://pandas.pydata.org/>`__ (0.20 or later)
 
 
diff --git a/condarecipe/larray/meta.yaml b/condarecipe/larray/meta.yaml
index 898acd850..e48b28c1a 100644
--- a/condarecipe/larray/meta.yaml
+++ b/condarecipe/larray/meta.yaml
@@ -16,11 +16,11 @@ build:
 
 requirements:
   host:
-    - python
+    - python >=3.7
     - pip
 
   run:
-    - python
+    - python >=3.7
     - numpy >=1.22
     - pandas >=0.20
 
diff --git a/doc/requirements.txt b/doc/requirements.txt
index 1110aa6f3..23ba68627 100644
--- a/doc/requirements.txt
+++ b/doc/requirements.txt
@@ -1,6 +1,6 @@
 # dependencies to run larray tutorials (via nbsphinx)
-numpy >=1.13
-pandas
+numpy >=1.22
+pandas >=0.20
 matplotlib
 tables  # ==pytables
 openpyxl
diff --git a/doc/source/api.rst b/doc/source/api.rst
index 27b0d1a00..73e13c8c7 100644
--- a/doc/source/api.rst
+++ b/doc/source/api.rst
@@ -62,7 +62,6 @@ Modifying/Selecting
    Axis.i
    Axis.by
    Axis.rename
-   Axis.subaxis
    Axis.extend
    Axis.insert
    Axis.replace
diff --git a/doc/source/changes.rst b/doc/source/changes.rst
index c538556bb..98fac5871 100644
--- a/doc/source/changes.rst
+++ b/doc/source/changes.rst
@@ -1,6 +1,20 @@
 ﻿Change log
 ##########
 
+Version 0.34.1
+==============
+
+In development.
+
+CORE
+----
+.. include:: ./changes/version_0_34_1.rst.inc
+
+EDITOR
+------
+.. include:: ./changes/editor/version_0_34_1.rst.inc
+
+
 Version 0.34
 ============
 
diff --git a/doc/source/changes/version_0_34_1.rst.inc b/doc/source/changes/version_0_34_1.rst.inc
new file mode 100644
index 000000000..d98904e5c
--- /dev/null
+++ b/doc/source/changes/version_0_34_1.rst.inc
@@ -0,0 +1,37 @@
+﻿.. py:currentmodule:: larray
+
+
+Syntax changes
+^^^^^^^^^^^^^^
+
+* renamed ``Array.old_method_name()`` to :py:obj:`Array.new_method_name()` (closes :issue:`1`).
+
+* renamed ``old_argument_name`` argument of :py:obj:`Array.method_name()` to ``new_argument_name``.
+
+
+Backward incompatible changes
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+* other backward incompatible changes
+
+
+New features
+^^^^^^^^^^^^
+
+* added support for Python 3.11.
+
+* added support for stacking all arrays of a Session by simply doing: `stack(my_session)` instead of
+  `stack(my_session.items())` (closes :issue:`1057`).
+
+.. _misc:
+
+Miscellaneous improvements
+^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+* improved something.
+
+
+Fixes
+^^^^^
+
+* avoid warnings with recent versions of Pandas or Numpy (closes :issue:`1061`).
diff --git a/doc/source/check_api_doc.py b/doc/source/check_api_doc.py
index 3ad87c4ae..56fa7051a 100644
--- a/doc/source/check_api_doc.py
+++ b/doc/source/check_api_doc.py
@@ -182,8 +182,8 @@ def insert_element(self, name, obj):
                 self.funcs[name] = obj
         elif inspect.isclass(obj):
             if name in self.classes:
-                # FIXME: missing format or fstring
-                warnings.warn("Class '{}' was already present in '{}' module item and will be replaced")
+                warnings.warn(f"Class '{name}' was already present in '{self.module.__name__}' module item "
+                              f"and will be replaced")
             class_ = getattr(self.module, name)
             class_item = ClassItem(class_)
             self.classes[name] = class_item
@@ -262,17 +262,22 @@ def make_diff(left_api, right_api, include_deprecated=False):
     return diff_api
 
 
+def get_module_item(module_name):
+    try:
+        module = importlib.import_module(module_name)
+        return ModuleItem(module)
+    except ImportError as err:
+        print(f'module {module_name} could not be imported: {err}')
+        return err
+
+
 def get_public_api():
     public_api = {}
     for module_name in _modules:
-        try:
-            module = importlib.import_module(module_name)
-            module_item = ModuleItem(module)
+        module_item = get_module_item(module_name)
+        if isinstance(module_item, ModuleItem):
             module_item.auto_discovery()
-            public_api[module_name] = module_item
-        except ImportError as err:
-            print(f'module {module_name} could not be imported: {err}')
-            public_api[module_name] = err
+        public_api[module_name] = module_item
     return public_api
 
 
@@ -290,13 +295,9 @@ def get_autosummary_api():
 
     autosummary_api = {}
     for module_name in _modules:
-        try:
-            module = importlib.import_module(module_name)
-            module_item = ModuleItem(module)
+        module_item = get_module_item(module_name)
+        if isinstance(module_item, ModuleItem):
             autosummary_api[module_name] = module_item
-        except ImportError as err:
-            print(f'module {module_name} could not be imported: {err}')
-            autosummary_api[module_name] = err
 
     for generated_rst_file in os.listdir(output_dir):
         qualname, ext = os.path.splitext(generated_rst_file)
diff --git a/doc/source/conf.py b/doc/source/conf.py
index 81a0b4494..617c02439 100644
--- a/doc/source/conf.py
+++ b/doc/source/conf.py
@@ -1,6 +1,4 @@
 #!/usr/bin/env python3
-# -*- coding: utf-8 -*-
-#
 # LArray documentation build configuration file, created by
 # sphinx-quickstart on Wed Nov 26 12:22:43 2014.
 #
diff --git a/doc/source/contribute.rst b/doc/source/contribute.rst
index c8d255f4e..eee8378e5 100644
--- a/doc/source/contribute.rst
+++ b/doc/source/contribute.rst
@@ -23,8 +23,7 @@ It is built to various formats using `Sphinx <http://sphinx-doc.org/>`_
 and `nbsphinx <https://nbsphinx.readthedocs.io>`_.
 
 The unit tests are written using the `pytest library <https://docs.pytest.org>`_.
-The compliance with the PEP8 conventions is tested using the extension
-`pytest-pep8 <https://pypi.org/project/pytest-pep8/>`_.
+The compliance with the PEP8 conventions is tested using `ruff <https://github.com/astral-sh/ruff/>`_.
 
 Many editors and IDE exist to edit Python code and provide integration with version control tools (like git).
 A good IDE, such as PyCharm, can make many of the steps below much more efficient.
@@ -90,16 +89,15 @@ One of the easiest way to do it is via `Anaconda` or `Miniconda`:
 
 We'll now kick off a two-step process:
 
-1. Install the build dependencies
+1. Install the dependencies
 
 .. code-block:: none
 
-   # add 'conda-forge' channel (required to install some dependencies)
-   conda config --add channels conda-forge
-
-   # Create and activate the build environment
-   conda create -n larray_dev numpy pandas pytables pyqt qtpy matplotlib openpyxl xlsxwriter pytest pytest-pep8
+   # Create and activate the environment
+   conda create -n larray_dev numpy pandas pytables pyqt qtpy matplotlib openpyxl xlsxwriter pytest
    conda activate larray_dev
+   # Install ruff (as of September 2023, it is not available on Anaconda)
+   pip install ruff
 
 This will create the new environment, and not touch any of your existing environments,
 nor any existing Python installation.
@@ -114,7 +112,7 @@ To return to your root environment::
 
 See the full conda docs `here <http://conda.pydata.org/docs>`_.
 
-2. Build and install larray
+2. Install larray in "development mode"
 
 Install larray using the following command:
 
@@ -181,6 +179,12 @@ code conventions. Among others, this means:
 
 This summary should not prevent you from reading the PEP!
 
+You can check your code respects most of those conventions and some other style guidelines by running
+the following command in the project directory: ::
+
+  > ruff check .
+
+
 Step 3: Document your code
 ~~~~~~~~~~~~~~~~~~~~~~~~~~
 
@@ -262,8 +266,6 @@ Step 4: Test your code
 
 Our unit tests are written using the `pytest library <https://docs.pytest.org>`_
 and our tests modules are located in `/larray/tests/`.
-We also use its extension `pytest-pep8 <https://pypi.org/project/pytest-pep8/>`_
-to check if the code is PEP8 compliant.
 The pytest library is able to automatically detect and run unit tests
 as long as you respect some conventions:
 
@@ -359,8 +361,8 @@ To submit a pull request:
 #. Click ``Send Pull Request``.
 
 This request then goes to the repository maintainers, and they will review the code.
-Your modifications will also be automatically tested by running the *larray* test suite on
-`Travis-CI <https://travis-ci.org/>`__ continuous integration service. A pull request will only be
+Your modifications will also be automatically tested by running the *larray* test suite via Github actions
+continuous integration service. A pull request will only be
 considered for merging when you have an all 'green' build.
 If any tests are failing, then you will get a red 'X', where you can click through to see the individual failed tests.
 
diff --git a/doc/source/fetch_changelogs.py b/doc/source/fetch_changelogs.py
index 1a7a33b01..4ad903b95 100644
--- a/doc/source/fetch_changelogs.py
+++ b/doc/source/fetch_changelogs.py
@@ -1,6 +1,6 @@
 #!/usr/bin/env python3
 """
-Fetch release notes from satellite projects of the larray-project constellation to the main project
+Fetch release notes from satellite projects of the larray-project constellation to the main project.
 """
 
 from pathlib import Path
diff --git a/larray/__init__.py b/larray/__init__.py
index d86953aea..898ad2e2c 100644
--- a/larray/__init__.py
+++ b/larray/__init__.py
@@ -1,4 +1,4 @@
-__version__ = '0.34'
+__version__ = '0.34.1-dev'
 
 
 from larray.core.axis import Axis, AxisCollection, X
diff --git a/larray/core/array.py b/larray/core/array.py
index 9dc80e1c9..1722b8adb 100644
--- a/larray/core/array.py
+++ b/larray/core/array.py
@@ -1,5 +1,5 @@
 """
-Array class
+Array class.
 """
 
 # ? implement multi group in one axis getitem: lipro['P01,P02;P05'] <=> (lipro['P01,P02'], lipro['P05'])
@@ -122,7 +122,7 @@ def prod(array, *args, **kwargs) -> Union['Array', Scalar]:
 
 def cumsum(array, *args, **kwargs) -> Union['Array', Scalar]:
     r"""
-    Returns the cumulative sum of array elements.
+    Return the cumulative sum of array elements.
 
     See Also
     --------
@@ -133,7 +133,7 @@ def cumsum(array, *args, **kwargs) -> Union['Array', Scalar]:
 
 def cumprod(array, *args, **kwargs) -> Union['Array', Scalar]:
     r"""
-    Returns the cumulative product of array elements.
+    Return the cumulative product of array elements.
 
     See Also
     --------
@@ -172,7 +172,7 @@ def max(array, *args, **kwargs) -> Union['Array', Scalar]:
 
 def mean(array, *args, **kwargs) -> Union['Array', Scalar]:
     r"""
-    Computes the arithmetic mean.
+    Compute the arithmetic mean.
 
     See Also
     --------
@@ -183,7 +183,7 @@ def mean(array, *args, **kwargs) -> Union['Array', Scalar]:
 
 def median(array, *args, **kwargs) -> Union['Array', Scalar]:
     r"""
-    Computes the median.
+    Compute the median.
 
     See Also
     --------
@@ -194,7 +194,7 @@ def median(array, *args, **kwargs) -> Union['Array', Scalar]:
 
 def percentile(array, *args, **kwargs) -> Union['Array', Scalar]:
     r"""
-    Computes the qth percentile of the data along the specified axis.
+    Compute the qth percentile of the data along the specified axis.
 
     See Also
     --------
@@ -206,7 +206,7 @@ def percentile(array, *args, **kwargs) -> Union['Array', Scalar]:
 # not commutative
 def ptp(array, *args, **kwargs) -> Union['Array', Scalar]:
     r"""
-    Returns the range of values (maximum - minimum).
+    Return the range of values (maximum - minimum).
 
     See Also
     --------
@@ -217,7 +217,7 @@ def ptp(array, *args, **kwargs) -> Union['Array', Scalar]:
 
 def var(array, *args, **kwargs) -> Union['Array', Scalar]:
     r"""
-    Computes the variance.
+    Compute the variance.
 
     See Also
     --------
@@ -228,7 +228,7 @@ def var(array, *args, **kwargs) -> Union['Array', Scalar]:
 
 def std(array, *args, **kwargs) -> Union['Array', Scalar]:
     r"""
-    Computes the standard deviation.
+    Compute the standard deviation.
 
     See Also
     --------
@@ -238,7 +238,7 @@ def std(array, *args, **kwargs) -> Union['Array', Scalar]:
 
 
 def concat(arrays, axis=0, dtype=None):
-    r"""Concatenate arrays along axis
+    r"""Concatenate arrays along axis.
 
     Parameters
     ----------
@@ -307,7 +307,7 @@ def __init__(self, array):
         next_data_func = data_iter.__next__
         res_axes = array.axes[1:]
         # this case should not happen (handled by the fastpath in Array.__iter__)
-        assert len(res_axes) > 0
+        assert len(res_axes) > 0  # noqa: S101
 
         def next_func():
             return Array(next_data_func(), res_axes)
@@ -646,6 +646,7 @@ class ArrayPositionalIndexer:
     a1   b0  12  14
     a1   b1  16  18
     """
+
     __slots__ = ('array',)
 
     def __init__(self, array):
@@ -722,6 +723,7 @@ class ArrayPointsIndexer:
         ...
     ValueError: all combined keys should have the same length
     """
+
     __slots__ = ('array',)
 
     def __init__(self, array):
@@ -783,6 +785,7 @@ class ArrayFlatIndicesIndexer:
     c  c0  c1
        42  30
     """
+
     __slots__ = ('array',)
 
     def __init__(self, array):
@@ -844,6 +847,7 @@ class ArrayPositionalPointsIndexer:
     a0_b0   0   1   2   3
     a1_b2  20  21  22  23
     """
+
     __slots__ = ('array',)
 
     def __init__(self, array):
@@ -858,7 +862,7 @@ def __setitem__(self, key, value):
 
 def get_axis(obj, i):
     r"""
-    Returns an axis according to its position.
+    Return an axis according to its position.
 
     Parameters
     ----------
@@ -904,7 +908,7 @@ def get_axis(obj, i):
     'out': {'value': None, 'doc': """
         out : Array, optional
             Alternate output array in which to place the result. It must have the same shape as the expected output and
-            its type is preserved (e.g., if dtype(out) is float, the result will consist of 0.0’s and 1.0’s).
+            its type is preserved (e.g., if dtype(out) is float, the result will consist of 0.0's and 1.0's).
             Axes and labels can be different, only the shape matters. Defaults to None (create a new array)."""},
     'ddof': {'value': 1, 'doc': """
         ddof : int, optional
@@ -1125,7 +1129,7 @@ def nan_equal(a1, a2):
 
 def _handle_meta(meta, title):
     """
-    Make sure meta is either None or a Metadata instance
+    Make sure meta is either None or a Metadata instance.
     """
     if title is not None:
         if meta is None:
@@ -1140,6 +1144,13 @@ def _handle_meta(meta, title):
                         f"instead of {type(meta).__name__}")
     return Metadata(meta)
 
+# This prevents a warning in Pandas 1.4 <= version < 2.0 for arrays with object
+# dtype which contain only numeric values. We force Pandas 2.0 behavior
+# (ie use object dtype instead of inferring). See issue #1061.
+def np_array_to_pd_index(array, name=None, tupleize_cols=True):
+    dtype = None if array.dtype.kind != 'O' else object
+    return pd.Index(array, dtype=dtype, name=name, tupleize_cols=tupleize_cols)
+
 
 class Array(ABCArray):
     r"""
@@ -1235,6 +1246,7 @@ class Array(ABCArray):
           M  10   9   8
           F  10  11  12
     """
+
     __slots__ = ('data', 'axes', '_meta')
 
     def __init__(self, data, axes=None, title=None, meta=None, dtype=None):
@@ -1273,7 +1285,7 @@ def title(self, title):
 
     @property
     def meta(self) -> Metadata:
-        r"""Returns metadata of the array.
+        r"""Return metadata of the array.
 
         Returns
         -------
@@ -1292,7 +1304,7 @@ def meta(self, meta):
     # TODO: implement wildcard argument to avoid producing the combined labels
     def nonzero(self) -> Tuple[IGroup, ...]:
         r"""
-        Returns the indices of the elements that are non-zero.
+        Return the indices of the elements that are non-zero.
 
         Specifically, it returns a tuple of arrays (one for each dimension)
         containing the indices of the non-zero elements in that dimension.
@@ -1493,7 +1505,7 @@ def ipoints(self) -> ArrayPositionalPointsIndexer:
 
     def to_frame(self, fold_last_axis_name=False, dropna=None) -> pd.DataFrame:
         r"""
-        Converts an Array into a Pandas DataFrame.
+        Convert an Array into a Pandas DataFrame.
 
         Parameters
         ----------
@@ -1538,17 +1550,17 @@ def to_frame(self, fold_last_axis_name=False, dropna=None) -> pd.DataFrame:
         a1 b0    4   5
            b1    6   7
         """
-        columns = pd.Index(self.axes[-1].labels)
-        if not fold_last_axis_name:
-            columns.name = self.axes[-1].name
+        last_name = self.axes[-1].name
+        columns_name = None if fold_last_axis_name else last_name
+        columns = np_array_to_pd_index(self.axes[-1].labels, name=columns_name)
         if self.ndim > 1:
             axes_names = self.axes.names[:-1]
             if fold_last_axis_name:
                 tmp = axes_names[-1] if axes_names[-1] is not None else ''
-                if self.axes[-1].name:
-                    axes_names[-1] = f"{tmp}\\{self.axes[-1].name}"
+                if last_name:
+                    axes_names[-1] = f"{tmp}\\{last_name}"
             if self.ndim == 2:
-                index = pd.Index(data=self.axes[0].labels, name=axes_names[0])
+                index = np_array_to_pd_index(self.axes[0].labels, name=axes_names[0])
             else:
                 index = pd.MultiIndex.from_product(self.axes.labels[:-1], names=axes_names)
         else:
@@ -1565,7 +1577,7 @@ def to_frame(self, fold_last_axis_name=False, dropna=None) -> pd.DataFrame:
 
     def to_series(self, name=None, dropna=False) -> pd.Series:
         r"""
-        Converts an Array into a Pandas Series.
+        Convert an Array into a Pandas Series.
 
         Parameters
         ----------
@@ -1632,7 +1644,9 @@ def to_series(self, name=None, dropna=False) -> pd.Series:
         elif self.ndim == 1:
             axis = self.axes[0]
             # Note that string labels will be converted to object dtype in the process
-            index = pd.Index(axis.labels, name=axis.name, tupleize_cols=False)
+            # and label arrays with object dtype containing only numeric values will keep
+            # the object dtype.
+            index = np_array_to_pd_index(axis.labels, name=axis.name, tupleize_cols=False)
         else:
             index = pd.MultiIndex.from_product(self.axes.labels, names=self.axes.names)
         series = pd.Series(self.data.reshape(-1), index, name=name)
@@ -1798,12 +1812,12 @@ def __bool__(self):
     # TODO: either support a list (of axes names) as first argument here (and set_labels)
     #       or don't support that in set_axes
     def rename(self, renames=None, to=None, inplace=False, **kwargs) -> 'Array':
-        r"""Renames axes of the array.
+        r"""Rename axes of the array.
 
         Parameters
         ----------
         renames : axis ref or dict {axis ref: str} or list of tuple (axis ref, str)
-            Renames to apply. If a single axis reference is given, the `to` argument must be used.
+            Rename to apply. If a single axis reference is given, the `to` argument must be used.
         to : str or Axis
             New name if `renames` contains a single axis reference.
         **kwargs : str or Axis
@@ -2196,7 +2210,7 @@ def align(self, other, join='outer', fill_value=nan, axes=None) -> Tuple['Array'
 
     @deprecate_kwarg('reverse', 'ascending', {True: False, False: True})
     def sort_values(self, key=None, axis=None, ascending=True) -> 'Array':
-        r"""Sorts values of the array.
+        r"""Sort values of the array.
 
         Parameters
         ----------
@@ -2319,7 +2333,7 @@ def sort_values(self, key=None, axis=None, ascending=True) -> 'Array':
 
     @deprecate_kwarg('reverse', 'ascending', {True: False, False: True})
     def sort_labels(self, axes=None, ascending=True) -> 'Array':
-        r"""Sorts labels of axes of the array.
+        r"""Sort labels of axes of the array.
 
         Parameters
         ----------
@@ -2381,7 +2395,7 @@ def sort_key(axis):
     sort_axis = renamed_to(sort_labels, 'sort_axis', raise_error=True)
     sort_axes = renamed_to(sort_labels, 'sort_axes')
 
-    # todo : set returned type to Union['Array', np.ndarray, Scalar] ?
+    # TODO: set returned type to Union['Array', np.ndarray, Scalar] ?
     def __getitem__(self, key, collapse_slices=False, translate_key=True, points=False) -> Union['Array', Scalar]:
         raw_broadcasted_key, res_axes, transpose_indices = \
             self.axes._key_to_raw_and_axes(key, collapse_slices, translate_key, points, wildcard=False)
@@ -2404,8 +2418,11 @@ def __setitem__(self, key, value, collapse_slices=True, translate_key=True, poin
         raw_broadcasted_key, target_axes, _ = \
             self.axes._key_to_raw_and_axes(key, collapse_slices, translate_key, points, wildcard=True)
         if isinstance(value, Array):
-            value = value.broadcast_with(target_axes, check_compatible=True)
-
+            # None target_axes can happen when setting a single "cell"/value with an Array (of size 1)
+            if target_axes is not None:
+                value = value.broadcast_with(target_axes, check_compatible=True)
+            else:
+                target_axes = []
             # replace incomprehensible error message "could not broadcast input array from shape XX into shape YY"
             # for users by "incompatible axes"
             extra_axes = [axis for axis in value.axes - target_axes if len(axis) > 1]
@@ -2429,7 +2446,7 @@ def __setitem__(self, key, value, collapse_slices=True, translate_key=True, poin
 
     def set(self, value, **kwargs) -> None:
         r"""
-        Sets a subset of array to value.
+        Set a subset of array to value.
 
         * all common axes must be either of length 1 or the same length
         * extra axes in value must be of length 1
@@ -2541,7 +2558,7 @@ def reshape_like(self, target) -> 'Array':
 
     def broadcast_with(self, target, check_compatible=False) -> 'Array':
         r"""
-        Returns an array that is (NumPy) broadcastable with target.
+        Return an array that is (NumPy) broadcastable with target.
 
         * all common axes must be either of length 1 or the same length
         * extra axes in source can have any length and will be moved to the
@@ -2872,7 +2889,7 @@ def dump(self, header=True, wide=True, value_name='value', light=False, axes_nam
     # defaults to 'auto' (ie collapse by default), can be set to False to
     # force a copy and to True to raise an exception if a view is not possible.
     def filter(self, collapse=False, **kwargs) -> 'Array':
-        r"""Filters the array along the axes given as keyword arguments.
+        r"""Filter the array along the axes given as keyword arguments.
 
         The *collapse* argument determines whether consecutive ranges should
         be collapsed to slices, which is more efficient and returns a view
@@ -3030,11 +3047,10 @@ def _group_aggregate(self, op, items, keepaxes=False, out=None, **kwargs) -> 'Ar
                 res = res_data
         return res
 
-    # todo : not sure about the returned type
+    # TODO: not sure about the returned type
     def _prepare_aggregate(self, op, args, kwargs=None, commutative=False, stack_depth=1) \
             -> Union[List[Union[LGroup, Axis]], AxisCollection]:
-        r"""converts args to keys & LGroup and kwargs to LGroup"""
-
+        r"""Convert args to keys & LGroup and kwargs to LGroup."""
         if kwargs is None:
             kwargs_items = []
         else:
@@ -3233,7 +3249,7 @@ def with_total(self, *args, op=sum, label='total', **kwargs) -> 'Array':
     # for the case where axis is None, we should return an NDGroup
     # so that arr[arr.labelofmin()] works even if the minimum is on ambiguous labels
     def labelofmin(self, axis=None) -> Union['Array', Tuple[Scalar, ...]]:
-        r"""Returns labels of the minimum values along a given axis.
+        r"""Return labels of the minimum values along a given axis.
 
         Parameters
         ----------
@@ -3275,7 +3291,7 @@ def labelofmin(self, axis=None) -> Union['Array', Tuple[Scalar, ...]]:
     argmin = renamed_to(labelofmin, 'argmin', raise_error=True)
 
     def indexofmin(self, axis=None) -> Union['Array', Tuple[int, ...]]:
-        r"""Returns indices of the minimum values along a given axis.
+        r"""Return indices of the minimum values along a given axis.
 
         Parameters
         ----------
@@ -3316,7 +3332,7 @@ def indexofmin(self, axis=None) -> Union['Array', Tuple[int, ...]]:
     posargmin = renamed_to(indexofmin, 'posargmin', raise_error=True)
 
     def labelofmax(self, axis=None) -> Union['Array', Tuple[Scalar, ...]]:
-        r"""Returns labels of the maximum values along a given axis.
+        r"""Return labels of the maximum values along a given axis.
 
         Parameters
         ----------
@@ -3358,7 +3374,7 @@ def labelofmax(self, axis=None) -> Union['Array', Tuple[Scalar, ...]]:
     argmax = renamed_to(labelofmax, 'argmax', raise_error=True)
 
     def indexofmax(self, axis=None) -> Union['Array', Tuple[int, ...]]:
-        r"""Returns indices of the maximum values along a given axis.
+        r"""Return indices of the maximum values along a given axis.
 
         Parameters
         ----------
@@ -3399,7 +3415,7 @@ def indexofmax(self, axis=None) -> Union['Array', Tuple[int, ...]]:
     posargmax = renamed_to(indexofmax, 'posargmax', raise_error=True)
 
     def labelsofsorted(self, axis=None, ascending=True, kind='quicksort') -> 'Array':
-        r"""Returns the labels that would sort this array.
+        r"""Return the labels that would sort this array.
 
         Performs an indirect sort along the given axis using the algorithm specified by the `kind` keyword. It returns
         an array of labels of the same shape as `a` that index data along the given axis in sorted order.
@@ -3447,7 +3463,7 @@ def labelsofsorted(self, axis=None, ascending=True, kind='quicksort') -> 'Array'
     argsort = renamed_to(labelsofsorted, 'argsort', raise_error=True)
 
     def indicesofsorted(self, axis=None, ascending=True, kind='quicksort') -> 'Array':
-        r"""Returns the indices that would sort this array.
+        r"""Return the indices that would sort this array.
 
         Performs an indirect sort along the given axis using the algorithm specified by the `kind` keyword. It returns
         an array of indices with the same axes as `a` that index data along the given axis in sorted order.
@@ -3503,7 +3519,7 @@ def indicesofsorted(self, axis=None, ascending=True, kind='quicksort') -> 'Array
     # XXX: implement expand=True? Unsure it is necessary now that we have zip_array_*
     # TODO: add support for groups in addition to entire axes
     def keys(self, axes=None, ascending=True) -> Product:
-        r"""Returns a view on the array labels along axes.
+        r"""Return a view on the array labels along axes.
 
         Parameters
         ----------
@@ -3583,7 +3599,7 @@ def keys(self, axes=None, ascending=True) -> Product:
     # TODO: add support for groups in addition to entire axes
     # TODO : not sure about the returned type
     def values(self, axes=None, ascending=True) -> Union[np.ndarray, List['Array'], ArrayPositionalIndexer]:
-        r"""Returns a view on the values of the array along axes.
+        r"""Return a view on the values of the array along axes.
 
         Parameters
         ----------
@@ -3678,7 +3694,7 @@ def values(self, axes=None, ascending=True) -> Union[np.ndarray, List['Array'],
     # TODO: we currently return a tuple of groups even for 1D arrays, which can be both a bad or a good thing.
     #       if we returned an NDGroup in all cases, it would solve the problem
     def items(self, axes=None, ascending=True) -> SequenceZip:
-        r"""Returns a (label, value) view of the array along axes.
+        r"""Return a (label, value) view of the array along axes.
 
         Parameters
         ----------
@@ -3767,12 +3783,12 @@ def iflat(self) -> ArrayFlatIndicesIndexer:
     iflat.__doc__ = ArrayFlatIndicesIndexer.__doc__
 
     def copy(self) -> 'Array':
-        r"""Returns a copy of the array. """
+        r"""Return a copy of the array."""
         return Array(self.data.copy(), axes=self.axes[:], meta=self.meta)
 
     # XXX: we might want to implement this using .groupby().first()
     def unique(self, axes=None, sort=False, sep='_') -> 'Array':
-        r"""Returns unique values (optionally along axes)
+        r"""Return unique values (optionally along axes).
 
         Parameters
         ----------
@@ -3866,7 +3882,7 @@ def unique(self, axes=None, sort=False, sep='_') -> 'Array':
 
     @property
     def info(self) -> str:
-        r"""Describes an Array (metadata + shape and labels for each axis).
+        r"""Describe an Array (metadata + shape and labels for each axis).
 
         Returns
         -------
@@ -3898,7 +3914,7 @@ def info(self) -> str:
         return ReprString(str_info)
 
     def ratio(self, *axes) -> 'Array':
-        r"""Returns an array with all values divided by the sum of values along given axes.
+        r"""Return an array with all values divided by the sum of values along given axes.
 
         Parameters
         ----------
@@ -3991,7 +4007,7 @@ def ratio(self, *axes) -> 'Array':
     def rationot0(self, *axes) -> 'Array':
         # part of the doctest is skipped because it produces a warning we do not want to have to handle within the
         # doctest and cannot properly ignore
-        r"""Returns an Array with values array / array.sum(axes) where the sum is not 0, 0 otherwise.
+        r"""Return an Array with values array / array.sum(axes) where the sum is not 0, 0 otherwise.
 
         Parameters
         ----------
@@ -4033,7 +4049,7 @@ def rationot0(self, *axes) -> 'Array':
         return self.divnot0(self.sum(*axes))
 
     def percent(self, *axes) -> 'Array':
-        r"""Returns an array with values given as percent of the total of all values along given axes.
+        r"""Return an array with values given as percent of the total of all values along given axes.
 
         Parameters
         ----------
@@ -4370,7 +4386,7 @@ def any_by(self, *args, **kwargs) -> Union['Array', Scalar]:
     def sum(self, *args, **kwargs) -> Union['Array', Scalar]:
         r"""{signature}
 
-        Computes the sum of array elements along given axes/groups.
+        Compute the sum of array elements along given axes/groups.
 
         {parameters}
 
@@ -4435,7 +4451,7 @@ def sum(self, *args, **kwargs) -> Union['Array', Scalar]:
     def sum_by(self, *args, **kwargs) -> Union['Array', Scalar]:
         r"""{signature}
 
-        Computes the sum of array elements for the given axes/groups.
+        Compute the sum of array elements for the given axes/groups.
 
         {parameters}
 
@@ -4498,7 +4514,7 @@ def sum_by(self, *args, **kwargs) -> Union['Array', Scalar]:
     def prod(self, *args, **kwargs) -> Union['Array', Scalar]:
         r"""{signature}
 
-        Computes the product of array elements along given axes/groups.
+        Compute the product of array elements along given axes/groups.
 
         {parameters}
 
@@ -4564,7 +4580,7 @@ def prod(self, *args, **kwargs) -> Union['Array', Scalar]:
     def prod_by(self, *args, **kwargs) -> Union['Array', Scalar]:
         r"""{signature}
 
-        Computes the product of array elements for the given axes/groups.
+        Compute the product of array elements for the given axes/groups.
 
         {parameters}
 
@@ -4876,7 +4892,7 @@ def max_by(self, *args, **kwargs) -> Union['Array', Scalar]:
     def mean(self, *args, **kwargs) -> Union['Array', Scalar]:
         r"""{signature}
 
-        Computes the arithmetic mean.
+        Compute the arithmetic mean.
 
         {parameters}
 
@@ -4942,7 +4958,7 @@ def mean(self, *args, **kwargs) -> Union['Array', Scalar]:
     def mean_by(self, *args, **kwargs) -> Union['Array', Scalar]:
         r"""{signature}
 
-        Computes the arithmetic mean.
+        Compute the arithmetic mean.
 
         {parameters}
 
@@ -5005,7 +5021,7 @@ def mean_by(self, *args, **kwargs) -> Union['Array', Scalar]:
     def median(self, *args, **kwargs) -> Union['Array', Scalar]:
         r"""{signature}
 
-        Computes the arithmetic median.
+        Compute the arithmetic median.
 
         {parameters}
 
@@ -5075,7 +5091,7 @@ def median(self, *args, **kwargs) -> Union['Array', Scalar]:
     def median_by(self, *args, **kwargs) -> Union['Array', Scalar]:
         r"""{signature}
 
-        Computes the arithmetic median.
+        Compute the arithmetic median.
 
         {parameters}
 
@@ -5152,7 +5168,7 @@ def percentile(self, q, *args,
                    **kwargs) -> Union['Array', Scalar]:
         r"""{signature}
 
-        Computes the qth percentile of the data along the specified axis.
+        Compute the qth percentile of the data along the specified axis.
 
         {parameters}
 
@@ -5253,7 +5269,7 @@ def percentile_by(self, q, *args,
                       **kwargs) -> Union['Array', Scalar]:
         r"""{signature}
 
-        Computes the qth percentile of the data for the specified axis.
+        Compute the qth percentile of the data for the specified axis.
 
         {parameters}
 
@@ -5347,9 +5363,9 @@ def compute_percentile(q):
     def ptp(self, *args, out=_kwarg_agg['out']['value'], **kwargs) -> Union['Array', Scalar]:
         r"""{signature}
 
-        Returns the range of values (maximum - minimum).
+        Return the range of values (maximum - minimum).
 
-        The name of the function comes from the acronym for ‘peak to peak’.
+        The name of the function comes from the acronym for `peak to peak`.
 
         {parameters}
 
@@ -5411,7 +5427,7 @@ def ptp(self, *args, out=_kwarg_agg['out']['value'], **kwargs) -> Union['Array',
     def var(self, *args, **kwargs) -> Union['Array', Scalar]:
         r"""{signature}
 
-        Computes the unbiased variance.
+        Compute the unbiased variance.
 
         Normalized by N-1 by default. This can be changed using the ddof argument.
 
@@ -5475,7 +5491,7 @@ def var(self, *args, **kwargs) -> Union['Array', Scalar]:
     def var_by(self, *args, **kwargs) -> Union['Array', Scalar]:
         r"""{signature}
 
-        Computes the unbiased variance.
+        Compute the unbiased variance.
 
         Normalized by N-1 by default. This can be changed using the ddof argument.
 
@@ -5539,7 +5555,7 @@ def var_by(self, *args, **kwargs) -> Union['Array', Scalar]:
     def std(self, *args, **kwargs) -> Union['Array', Scalar]:
         r"""{signature}
 
-        Computes the sample standard deviation.
+        Compute the sample standard deviation.
 
         Normalized by N-1 by default. This can be changed using the ddof argument.
 
@@ -5604,7 +5620,7 @@ def std(self, *args, **kwargs) -> Union['Array', Scalar]:
     def std_by(self, *args, **kwargs) -> Union['Array', Scalar]:
         r"""{signature}
 
-        Computes the sample standard deviation.
+        Compute the sample standard deviation.
 
         Normalized by N-1 by default. This can be changed using the ddof argument.
 
@@ -5667,7 +5683,7 @@ def std_by(self, *args, **kwargs) -> Union['Array', Scalar]:
     # cumulative aggregates
     def cumsum(self, axis=-1) -> Union['Array', Scalar]:
         r"""
-        Returns the cumulative sum of array elements along an axis.
+        Return the cumulative sum of array elements along an axis.
 
         Parameters
         ----------
@@ -5715,7 +5731,7 @@ def cumsum(self, axis=-1) -> Union['Array', Scalar]:
 
     def cumprod(self, axis=-1) -> Union['Array', Scalar]:
         r"""
-        Returns the cumulative product of array elements.
+        Return the cumulative product of array elements.
 
         Parameters
         ----------
@@ -5779,15 +5795,10 @@ def opmethod(self, other) -> 'Array':
 
             # we could pass scalars through asarray too but it is too costly performance-wise for only suppressing one
             # isscalar test and an if statement.
-            # TODO: ndarray should probably be converted to larrays because that would harmonize broadcasting rules, but
-            # it makes some tests fail for some reason.
+            # TODO: ndarray should probably be converted to larrays too because that would harmonize broadcasting rules,
+            #       but it makes some tests fail for some reason.
             if isinstance(other, (list, Axis)):
                 other = asarray(other)
-            elif other is not None and not isinstance(other, (Array, np.ndarray)) and not np.isscalar(other):
-                # support for inspect.signature
-                # FIXME: this should only be the case for __eq__. For other operations, we should
-                #        probably raise a TypeError (or return NotImplemented???)
-                return False
 
             if isinstance(other, Array):
                 # TODO: first test if it is not already broadcastable
@@ -5797,9 +5808,13 @@ def opmethod(self, other) -> 'Array':
                     res_axes = self.axes
                 else:
                     (self_data, other_data), res_axes = raw_broadcastable((self, other))
-            else:
+            # We need to check for None explicitly because we consider None as a valid scalar, while numpy does not.
+            # i.e. we consider "arr == None" as valid code
+            elif isinstance(other, np.ndarray) or np.isscalar(other) or other is None:
                 self_data, other_data = self.data, other
                 res_axes = self.axes
+            else:
+                return NotImplemented
             return Array(super_method(self_data, other_data), res_axes)
         opmethod.__name__ = fullname
         return opmethod
@@ -5840,7 +5855,7 @@ def opmethod(self, other) -> 'Array':
 
     def __matmul__(self, other) -> 'Array':
         r"""
-        Overrides operator @ for matrix multiplication.
+        Override operator @ for matrix multiplication.
 
         Notes
         -----
@@ -5962,7 +5977,7 @@ def __float__(self):
     @deprecate_kwarg('nan_equals', 'nans_equal')
     def equals(self, other, rtol=0, atol=0, nans_equal=False, check_axes=False) -> bool:
         r"""
-        Compares this array with another array and returns True if they have the same axes and elements,
+        Compare this array with another array and returns True if they have the same axes and elements,
         False otherwise.
 
         Parameters
@@ -5986,7 +6001,7 @@ def equals(self, other, rtol=0, atol=0, nans_equal=False, check_axes=False) -> b
         Returns
         -------
         bool
-            Returns True if this array is equal to other.
+            Return True if this array is equal to other.
 
         See Also
         --------
@@ -6095,7 +6110,7 @@ def equals(self, other, rtol=0, atol=0, nans_equal=False, check_axes=False) -> b
     def allclose(self, other: Any, rtol: float = 1e-05, atol: float = 1e-08, nans_equal: bool = True,
                  check_axes: bool = False) -> bool:
         """
-        Compares this array with another array and returns True if they are element-wise equal within a tolerance.
+        Compare this array with another array and returns True if they are element-wise equal within a tolerance.
 
         The tolerance values are positive, typically very small numbers.
         The relative difference (rtol * abs(other)) and the absolute difference atol are added together to compare
@@ -6124,7 +6139,7 @@ def allclose(self, other: Any, rtol: float = 1e-05, atol: float = 1e-08, nans_eq
         Returns
         -------
         bool
-            Returns True if the two arrays are equal within the given tolerance; False otherwise.
+            Return True if the two arrays are equal within the given tolerance; False otherwise.
 
         See Also
         --------
@@ -6168,7 +6183,7 @@ def allclose(self, other: Any, rtol: float = 1e-05, atol: float = 1e-08, nans_eq
     @deprecate_kwarg('nan_equals', 'nans_equal')
     def eq(self, other, rtol=0, atol=0, nans_equal=False) -> 'Array':
         """
-        Compares this array with another array element-wise and returns an array of booleans.
+        Compare this array with another array element-wise and returns an array of booleans.
 
         Parameters
         ----------
@@ -6257,7 +6272,7 @@ def general_isnan(a):
 
     def isin(self, test_values, assume_unique=False, invert=False) -> 'Array':
         r"""
-        Computes whether each element of this array is in `test_values`. Returns a boolean array of the same shape as
+        Compute whether each element of this array is in `test_values`. Return a boolean array of the same shape as
         this array that is True where the array element is in `test_values` and False otherwise.
 
         Parameters
@@ -6300,7 +6315,7 @@ def isin(self, test_values, assume_unique=False, invert=False) -> 'Array':
     def divnot0(self, other) -> 'Array':
         # part of the doctest is skipped because it produces a warning we do not want to have to handle within the
         # doctest and cannot properly ignore
-        r"""Divides this array by other, but returns 0.0 where other is 0.
+        r"""Divide this array by other, but return 0.0 where other is 0.
 
         Parameters
         ----------
@@ -6356,7 +6371,7 @@ def divnot0(self, other) -> 'Array':
     # XXX: rename/change to "add_axes" ?
     # TODO: add a flag copy=True to force a new array.
     def expand(self, target_axes=None, out=None, readonly=False) -> 'Array':
-        r"""Expands this array to target_axes.
+        r"""Expand this array to target_axes.
 
         Target axes will be added to this array if not present.
         In most cases this function is not needed because LArray can do operations with arrays having different
@@ -6445,7 +6460,7 @@ def expand(self, target_axes=None, out=None, readonly=False) -> 'Array':
         return out
 
     def append(self, axis, value, label=None) -> 'Array':
-        r"""Adds a value to this array along an axis.
+        r"""Add a value to this array along an axis.
 
         Parameters
         ----------
@@ -6527,7 +6542,7 @@ def append(self, axis, value, label=None) -> 'Array':
     extend = renamed_to(append, 'extend')
 
     def prepend(self, axis, value, label=None) -> 'Array':
-        r"""Adds an array before this array along an axis.
+        r"""Add an array before this array along an axis.
 
         The two arrays must have compatible axes.
 
@@ -6577,7 +6592,7 @@ def prepend(self, axis, value, label=None) -> 'Array':
         return self.insert(value, before=IGroup(0, axis=axis), label=label)
 
     def insert(self, value, before=None, after=None, pos=None, axis=None, label=None) -> 'Array':
-        r"""Inserts value in array along an axis.
+        r"""Insert value in array along an axis.
 
         Parameters
         ----------
@@ -6642,7 +6657,6 @@ def insert(self, value, before=None, after=None, pos=None, axis=None, label=None
          a0   0    42    43   1   2
          a1   3    44    45   4   5
         """
-
         # XXX: unsure we should have arr1.insert(arr3, before='b1,b2') result in (see unit tests):
 
         # a\b  b0  b0.1  b1  b0.2  b2
@@ -7030,7 +7044,7 @@ def clip(self, minval=None, maxval=None, out=None) -> 'Array':
     def to_csv(self, filepath, sep=',', na_rep='', wide=True, value_name='value', dropna=None,
                dialect='default', **kwargs) -> None:
         r"""
-        Writes array to a csv file.
+        Write array to a csv file.
 
         Parameters
         ----------
@@ -7101,7 +7115,7 @@ def to_csv(self, filepath, sep=',', na_rep='', wide=True, value_name='value', dr
 
     def to_hdf(self, filepath, key) -> None:
         r"""
-        Writes array to a HDF file.
+        Write array to a HDF file.
 
         A HDF file can contain multiple arrays.
         The 'key' parameter is a unique identifier for the array.
@@ -7130,7 +7144,7 @@ def to_hdf(self, filepath, key) -> None:
 
         Save an array in a specific HDF group
 
-        >>> a.to_hdf('test.h5', 'arrays/a')  # doctest: +SKIP
+        >>> a.to_hdf('test.h5', 'arrays/a')   # doctest: +SKIP
         """
         key = _translate_group_key_hdf(key)
         with LHDFStore(filepath) as store:
@@ -7142,7 +7156,7 @@ def to_hdf(self, filepath, key) -> None:
 
     def to_stata(self, filepath_or_buffer, **kwargs) -> None:
         r"""
-        Writes array to a Stata .dta file.
+        Write array to a Stata .dta file.
 
         Parameters
         ----------
@@ -7168,7 +7182,7 @@ def to_stata(self, filepath_or_buffer, **kwargs) -> None:
                   0       BE    F
                   1       FR    M
                   2       FR    F
-        >>> arr.to_stata('test.dta')
+        >>> arr.to_stata('test.dta')      # doctest: +SKIP
         """
         self.to_frame().to_stata(filepath_or_buffer, **kwargs)
 
@@ -7176,7 +7190,7 @@ def to_stata(self, filepath_or_buffer, **kwargs) -> None:
     def to_excel(self, filepath=None, sheet=None, position='A1', overwrite_file=False, clear_sheet=False,
                  header=True, transpose=False, wide=True, value_name='value', engine=None, *args, **kwargs) -> None:
         r"""
-        Writes array in the specified sheet of specified excel workbook.
+        Write array in the specified sheet of specified excel workbook.
 
         Parameters
         ----------
@@ -7280,7 +7294,7 @@ def to_excel(self, filepath=None, sheet=None, position='A1', overwrite_file=Fals
             pd_obj.to_excel(filepath, sheet, *args, engine=engine, **kwargs)
 
     def to_clipboard(self, *args, **kwargs) -> None:
-        r"""Sends the content of the array to clipboard.
+        r"""Send the content of the array to the clipboard.
 
         Using to_clipboard() makes it possible to paste the content of the array into a file (Excel, ascii file,...).
 
@@ -7328,7 +7342,7 @@ def to_clipboard(self, *args, **kwargs) -> None:
 
     @property
     def plot(self) -> PlotObject:
-        r"""Plots the data of the array into a graph (window pop-up).
+        r"""Plot the data of the array into a graph (window pop-up).
 
         The graph can be tweaked to achieve the desired formatting and can be saved to a .png file.
 
@@ -7455,7 +7469,7 @@ def plot(self) -> PlotObject:
 
     @property
     def shape(self) -> Tuple[int, ...]:
-        r"""Returns the shape of the array as a tuple.
+        r"""Return the shape of the array as a tuple.
 
         Returns
         -------
@@ -7472,7 +7486,7 @@ def shape(self) -> Tuple[int, ...]:
 
     @property
     def ndim(self) -> int:
-        r"""Returns the number of dimensions of the array.
+        r"""Return the number of dimensions of the array.
 
         Returns
         -------
@@ -7489,7 +7503,7 @@ def ndim(self) -> int:
 
     @property
     def size(self) -> int:
-        r"""Returns the number of elements in array.
+        r"""Return the number of elements in array.
 
         Returns
         -------
@@ -7506,7 +7520,7 @@ def size(self) -> int:
 
     @property
     def nbytes(self) -> int:
-        r"""Returns the number of bytes used to store the array in memory.
+        r"""Return the number of bytes used to store the array in memory.
 
         Returns
         -------
@@ -7523,7 +7537,7 @@ def nbytes(self) -> int:
 
     @property
     def memory_used(self) -> str:
-        r"""Returns the memory consumed by the array in human readable form.
+        r"""Return the memory consumed by the array in human readable form.
 
         Returns
         -------
@@ -7540,7 +7554,7 @@ def memory_used(self) -> str:
 
     @property
     def dtype(self) -> np.dtype:
-        r"""Returns the type of the data of the array.
+        r"""Return the type of the data of the array.
 
         Returns
         -------
@@ -7569,7 +7583,7 @@ def __array__(self, dtype=None):
 
     # TODO: this should be a thin wrapper around a method in AxisCollection
     def set_labels(self, axis=None, labels=None, inplace=False, **kwargs) -> 'Array':
-        r"""Replaces the labels of one or several axes of the array.
+        r"""Replace the labels of one or several axes of the array.
 
         Parameters
         ----------
@@ -7678,7 +7692,7 @@ def astype(self, dtype, order='K', casting='unsafe', subok=True, copy=True) -> '
     astype.__doc__ = np.ndarray.astype.__doc__
 
     def shift(self, axis, n=1) -> 'Array':
-        r"""Shifts the cells of the array n-times to the right along axis.
+        r"""Shift the cells of the array n-times to the right along axis.
 
         Parameters
         ----------
@@ -7721,7 +7735,7 @@ def shift(self, axis, n=1) -> 'Array':
             return self[:]
 
     def roll(self, axis=None, n=1) -> 'Array':
-        r"""Rolls the cells of the array n-times to the right along axis. Cells which would be pushed "outside of the
+        r"""Roll the cells of the array n-times to the right along axis. Cells which would be pushed "outside of the
         axis" are reintroduced on the opposite side of the axis.
 
         Parameters
@@ -7780,7 +7794,7 @@ def roll(self, axis=None, n=1) -> 'Array':
     # TODO: add support for groups as axis (like aggregates)
     # eg a.diff(X.year[2018:]) instead of a[2018:].diff(X.year)
     def diff(self, axis=-1, d=1, n=1, label='upper') -> 'Array':
-        r"""Calculates the n-th order discrete difference along a given axis.
+        r"""Compute the n-th order discrete difference along a given axis.
 
         The first order difference is given by out[n] = a[n + 1] - a[n] along the given axis, higher order differences
         are calculated by using diff recursively.
@@ -7845,7 +7859,7 @@ def diff(self, axis=-1, d=1, n=1, label='upper') -> 'Array':
     # XXX: this is called pct_change in Pandas (but returns the same results, not results * 100, which I find silly).
     # Maybe change_rate would be better (because growth is not always positive)?
     def growth_rate(self, axis=-1, d=1, label='upper') -> 'Array':
-        r"""Calculates the growth along a given axis.
+        r"""Compute the growth along a given axis.
 
         Roughly equivalent to a.diff(axis, d, label) / a[axis.i[:-d]]
 
@@ -7910,7 +7924,7 @@ def growth_rate(self, axis=-1, d=1, label='upper') -> 'Array':
         return Array(diff.data / shifted_array, diff.axes)
 
     def compact(self, display=False, name='array') -> 'Array':
-        r"""Detects and removes "useless" axes (ie axes for which values are constant over the whole axis)
+        r"""Detect and remove "useless" axes (ie axes for which values are constant over the whole axis).
 
         Parameters
         ----------
@@ -8041,7 +8055,7 @@ def combine_axes(self, axes=None, sep='_', wildcard=False) -> 'Array':
         assert isinstance(axes, dict)
 
         transposed_axes = self.axes[:]
-        for axes_to_combine, name in axes.items():
+        for axes_to_combine in axes.keys():
             # transpose all axes next to each other, using index of first axis
             axes_to_combine = self.axes[axes_to_combine]
             axes_indices = [transposed_axes.index(axis) for axis in axes_to_combine]
@@ -8054,7 +8068,7 @@ def combine_axes(self, axes=None, sep='_', wildcard=False) -> 'Array':
         return transposed.reshape(new_axes)
 
     def split_axes(self, axes=None, sep='_', names=None, regex=None, sort=False, fill_value=nan) -> 'Array':
-        r"""Split axes and returns a new array
+        r"""Split axes and returns a new array.
 
         Parameters
         ----------
@@ -8172,7 +8186,7 @@ def split_axes(self, axes=None, sep='_', names=None, regex=None, sort=False, fil
 
     def reverse(self, axes=None) -> 'Array':
         r"""
-        Reverse axes of an array
+        Reverse axes of an array.
 
         Parameters
         ----------
@@ -8463,7 +8477,7 @@ def larray_nan_equal(a1, a2):
 
 def asarray(a, meta=None) -> Array:
     r"""
-    Converts input as Array if possible.
+    Convert input as Array if possible.
 
     Parameters
     ----------
@@ -8529,7 +8543,7 @@ def wrapper(*args, **kwargs) -> Array:
 
 @_check_axes_argument
 def zeros(axes, title=None, dtype=float, order='C', meta=None) -> Array:
-    r"""Returns an array with the specified axes and filled with zeros.
+    r"""Return an array with the specified axes and filled with zeros.
 
     Parameters
     ----------
@@ -8575,7 +8589,7 @@ def zeros(axes, title=None, dtype=float, order='C', meta=None) -> Array:
 
 
 def zeros_like(array, title=None, dtype=None, order='K', meta=None) -> Array:
-    r"""Returns an array with the same axes as array and filled with zeros.
+    r"""Return an array with the same axes as array and filled with zeros.
 
     Parameters
     ----------
@@ -8611,7 +8625,7 @@ def zeros_like(array, title=None, dtype=None, order='K', meta=None) -> Array:
 
 @_check_axes_argument
 def ones(axes, title=None, dtype=float, order='C', meta=None) -> Array:
-    r"""Returns an array with the specified axes and filled with ones.
+    r"""Return an array with the specified axes and filled with ones.
 
     Parameters
     ----------
@@ -8647,7 +8661,7 @@ def ones(axes, title=None, dtype=float, order='C', meta=None) -> Array:
 
 
 def ones_like(array, title=None, dtype=None, order='K', meta=None) -> Array:
-    r"""Returns an array with the same axes as array and filled with ones.
+    r"""Return an array with the same axes as array and filled with ones.
 
     Parameters
     ----------
@@ -8684,7 +8698,7 @@ def ones_like(array, title=None, dtype=None, order='K', meta=None) -> Array:
 
 @_check_axes_argument
 def empty(axes, title=None, dtype=float, order='C', meta=None) -> Array:
-    r"""Returns an array with the specified axes and uninitialized (arbitrary) data.
+    r"""Return an array with the specified axes and uninitialized (arbitrary) data.
 
     Parameters
     ----------
@@ -8720,7 +8734,7 @@ def empty(axes, title=None, dtype=float, order='C', meta=None) -> Array:
 
 
 def empty_like(array, title=None, dtype=None, order='K', meta=None) -> Array:
-    r"""Returns an array with the same axes as array and uninitialized (arbitrary) data.
+    r"""Return an array with the same axes as array and uninitialized (arbitrary) data.
 
     Parameters
     ----------
@@ -8758,7 +8772,7 @@ def empty_like(array, title=None, dtype=None, order='K', meta=None) -> Array:
 
 # We cannot use @_check_axes_argument here because an integer fill_value would be considered as an error
 def full(axes, fill_value, title=None, dtype=None, order='C', meta=None) -> Array:
-    r"""Returns an array with the specified axes and filled with fill_value.
+    r"""Return an array with the specified axes and filled with fill_value.
 
     Parameters
     ----------
@@ -8810,7 +8824,7 @@ def full(axes, fill_value, title=None, dtype=None, order='C', meta=None) -> Arra
 
 
 def full_like(array, fill_value, title=None, dtype=None, order='K', meta=None) -> Array:
-    r"""Returns an array with the same axes and type as input array and filled with fill_value.
+    r"""Return an array with the same axes and type as input array and filled with fill_value.
 
     Parameters
     ----------
@@ -8856,7 +8870,7 @@ def full_like(array, fill_value, title=None, dtype=None, order='K', meta=None) -
 # XXX: would it be possible to generalize to multiple axes?
 def sequence(axis, initial=0, inc=None, mult=None, func=None, axes=None, title=None, meta=None) -> Array:
     r"""
-    Creates an array by sequentially applying modifications to the array along axis.
+    Create an array by sequentially applying modifications to the array along axis.
 
     The value for each label in axis will be given by sequentially transforming the value for the previous label.
     This transformation on the previous label value consists of applying the function "func" on that value if provided,
@@ -9137,7 +9151,7 @@ def ndrange(axes, start=0, title=None, dtype=int):
 
 @_check_axes_argument
 def ndtest(shape_or_axes, start=0, label_start=0, title=None, dtype=int, meta=None) -> Array:
-    r"""Returns test array with given shape.
+    r"""Return test array with given shape.
 
     Axes are named by single letters starting from 'a'.
     Axes labels are constructed using a '{axis_name}{label_pos}' pattern (e.g. 'a0').
@@ -9238,7 +9252,7 @@ def kth_diag_indices(shape, k):
 
 def diag(a, k=0, axes=(0, 1), ndim=2, split=True) -> Array:
     r"""
-    Extracts a diagonal or construct a diagonal array.
+    Extract a diagonal or construct a diagonal array.
 
     Parameters
     ----------
@@ -9319,7 +9333,7 @@ def diag(a, k=0, axes=(0, 1), ndim=2, split=True) -> Array:
 
 @_check_axes_argument
 def labels_array(axes, title=None, meta=None) -> Array:
-    r"""Returns an array with specified axes and the combination of
+    r"""Return an array with specified axes and the combination of
     corresponding labels as values.
 
     Parameters
@@ -9376,7 +9390,7 @@ def identity(axis):
 
 
 def eye(rows, columns=None, k=0, title=None, dtype=None, meta=None) -> Array:
-    r"""Returns a 2-D array with ones on the diagonal and zeros elsewhere.
+    r"""Return a 2-D array with ones on the diagonal and zeros elsewhere.
 
     Parameters
     ----------
@@ -9570,17 +9584,16 @@ def eye(rows, columns=None, k=0, title=None, dtype=None, meta=None) -> Array:
 @deprecate_kwarg('axis', 'axes')
 def stack(elements=None, axes=None, title=None, meta=None, dtype=None, res_axes=None, **kwargs) -> 'Array':
     r"""
-    Combines several arrays or sessions along an axis.
+    Combine several arrays or sessions along an axis.
 
     Parameters
     ----------
-    elements : tuple, list or dict.
+    elements : tuple, list, dict or Session.
         Elements to stack. Elements can be scalars, arrays, sessions, (label, value) pairs or a {label: value} mapping.
-        In the later case, axis must be defined and cannot be a name only, because we need to have labels order,
-        which the mapping does not provide.
 
-        Stacking sessions will return a new session containing the arrays of all sessions stacked together. An array
-        missing in a session will be replaced by NaN.
+        Stacking a single session will stack all its arrays in a single array.
+        Stacking several sessions will take the corresponding arrays in all the sessions and stack them, returning a
+        new session. An array missing in a session will be replaced by NaN.
     axes : str, Axis, Group or sequence of Axis, optional
         Axes to create. If None, defaults to a range() axis.
     title : str, optional
@@ -9721,7 +9734,7 @@ def stack(elements=None, axes=None, title=None, meta=None, dtype=None, res_axes=
     if kwargs:
         elements = kwargs.items()
 
-    if isinstance(elements, dict):
+    if isinstance(elements, (dict, Session)):
         elements = elements.items()
 
     if isinstance(elements, Array):
@@ -9729,6 +9742,10 @@ def stack(elements=None, axes=None, title=None, meta=None, dtype=None, res_axes=
             axes = -1
         axes = elements.axes[axes]
         items = elements.items(axes)
+    elif isinstance(elements, Session):
+        if axes is None:
+            axes = 'array'
+        items = elements.items()
     elif isinstance(elements, Iterable):
         if not isinstance(elements, Sequence):
             elements = list(elements)
@@ -9848,7 +9865,7 @@ def _equal_modulo_len1(shape1, shape2):
 # but if we had assigned axes names from the start (without dropping them) this wouldn't be a problem.
 def make_numpy_broadcastable(values, min_axes=None) -> Tuple[List[Array], AxisCollection]:
     r"""
-    Returns values where Arrays are (NumPy) broadcastable between them.
+    Return values where Arrays are (NumPy) broadcastable between them.
     For that to be possible, all common axes must be compatible (see Axis class documentation).
     Extra axes (in any array) can have any length.
 
@@ -9888,7 +9905,7 @@ def make_numpy_broadcastable(values, min_axes=None) -> Tuple[List[Array], AxisCo
 
 def raw_broadcastable(values, min_axes=None) -> Tuple[Tuple[Any, ...], AxisCollection]:
     r"""
-    same as make_numpy_broadcastable but returns numpy arrays
+    same as make_numpy_broadcastable but returns numpy arrays.
     """
     arrays, res_axes = make_numpy_broadcastable(values, min_axes=min_axes)
     raw = tuple(a.data if isinstance(a, Array) else a
@@ -9909,7 +9926,7 @@ def make_args_broadcastable(args, kwargs=None) -> Tuple[Any, Any, Any]:
 
 
 def zip_array_values(values, axes=None, ascending=True) -> SequenceZip:
-    r"""Returns a sequence as if simultaneously iterating on several arrays.
+    r"""Return a sequence as if simultaneously iterating on several arrays.
 
     Parameters
     ----------
@@ -10017,7 +10034,7 @@ def values_with_expand(value, axes, readonly=True, ascending=True):
 
 
 def zip_array_items(values, axes=None, ascending=True) -> SequenceZip:
-    r"""Returns a sequence as if simultaneously iterating on several arrays as well as the current iteration "key".
+    r"""Return a sequence as if simultaneously iterating on several arrays as well as the current iteration "key".
 
     Broadcasts all values against each other. Scalars are simply repeated.
 
diff --git a/larray/core/axis.py b/larray/core/axis.py
index e41ccf126..e7e66e5c0 100644
--- a/larray/core/axis.py
+++ b/larray/core/axis.py
@@ -91,6 +91,7 @@ class Axis(ABCAxis):
     >>> anonymous
     Axis([0, 1, 2, 3, 4], None)
     """
+
     __slots__ = ('name', '__mapping', '__sorted_keys', '__sorted_values', '_labels', '_length', '_iswildcard')
 
     # ticks instead of labels?
@@ -171,8 +172,7 @@ def _sorted_values(self) -> np.ndarray:
     @lazy_attribute
     def i(self) -> IGroupMaker:
         r"""
-        Allows to define a subset using positions along the axis
-        instead of labels.
+        Take a subset using positions along the axis instead of labels.
 
         Examples
         --------
@@ -194,7 +194,7 @@ def i(self) -> IGroupMaker:
     @property
     def labels(self) -> Iterable[Scalar]:
         r"""
-        labels of the axis.
+        Labels of the axis.
         """
         return self._labels
 
@@ -471,12 +471,12 @@ def group(self, *args, name=None):
         axis_name = self.name if self.name else 'axis'
         syntax = f'{axis_name}[{key}]'
         if name is not None:
-            syntax += f' >> {repr(name)}'
+            syntax += f' >> {name!r}'
         raise NotImplementedError(f'Axis.group is deprecated. Use {syntax} instead.')
 
     def all(self, name=None):
         r"""
-        (Deprecated) Returns a group containing all labels.
+        (Deprecated) Return a group containing all labels.
 
         Parameters
         ----------
@@ -485,12 +485,12 @@ def all(self, name=None):
         """
         axis_name = self.name if self.name else 'axis'
         group_name = name if name else 'all'
-        raise NotImplementedError(f'Axis.all is deprecated. Use {axis_name}[:] >> {repr(group_name)} instead.')
+        raise NotImplementedError(f'Axis.all is deprecated. Use {axis_name}[:] >> {group_name!r} instead.')
 
     # TODO: make this method private
     def subaxis(self, key) -> 'Axis':
         r"""
-        Returns an axis for a sub-array.
+        Return an axis for a sub-array.
 
         Parameters
         ----------
@@ -523,7 +523,7 @@ def subaxis(self, key) -> 'Axis':
 
     def iscompatible(self, other) -> bool:
         r"""
-        Checks if this axis is compatible with another axis.
+        Check if this axis is compatible with another axis.
 
         * two axes are compatible if they have compatible names and labels
         * names are compatible if they are the same or missing
@@ -573,7 +573,7 @@ def iscompatible(self, other) -> bool:
 
     def equals(self, other) -> bool:
         r"""
-        Checks if this axis is equal to another axis.
+        Check if this axis is equal to another axis.
         Two axes are equal if they have the same name and label(s).
 
         Parameters
@@ -652,7 +652,7 @@ def max(self) -> Scalar:
 
     def matching(self, deprecated=None, pattern=None, regex=None) -> LGroup:
         r"""
-        Returns a group with all the labels matching the specified pattern or regular expression.
+        Return a group with all the labels matching the specified pattern or regular expression.
 
         Parameters
         ----------
@@ -726,7 +726,7 @@ def matching(self, deprecated=None, pattern=None, regex=None) -> LGroup:
 
     def startingwith(self, prefix) -> LGroup:
         r"""
-        Returns a group with the labels starting with the specified string.
+        Return a group with the labels starting with the specified string.
 
         Parameters
         ----------
@@ -752,7 +752,7 @@ def startingwith(self, prefix) -> LGroup:
 
     def endingwith(self, suffix) -> LGroup:
         r"""
-        Returns a group with the labels ending with the specified string.
+        Return a group with the labels ending with the specified string.
 
         Parameters
         ----------
@@ -778,7 +778,7 @@ def endingwith(self, suffix) -> LGroup:
 
     def containing(self, substring) -> LGroup:
         r"""
-        Returns a group with all the labels containing the specified substring.
+        Return a group with all the labels containing the specified substring.
 
         Parameters
         ----------
@@ -808,7 +808,7 @@ def __iter__(self) -> Iterable[IGroup]:
 
     def __getitem__(self, key) -> Union[LGroup, List[Group], Tuple[Group, ...]]:
         r"""
-        Returns a group (list or unique element) of label(s) usable in .sum or .filter
+        Return a group (list or unique element) of label(s) usable in .sum or .filter.
 
         key is a label-based key (other axis, slice and fancy indexing are supported)
 
@@ -881,7 +881,7 @@ def _is_key_type_compatible(self, key) -> bool:
 
     def index(self, key) -> Union[int, np.ndarray, slice]:
         r"""
-        Translates a label key to its numerical index counterpart.
+        Translate a label key to its numerical index counterpart.
 
         Parameters
         ----------
@@ -1030,7 +1030,7 @@ def __repr__(self) -> str:
 
     def labels_summary(self) -> str:
         r"""
-        Returns a short representation of the labels.
+        Return a short representation of the labels.
 
         Examples
         --------
@@ -1101,14 +1101,14 @@ def opmethod(self, other):
 
     def __larray__(self):
         r"""
-        Returns axis as Array.
+        Return axis as Array.
         """
         from .array import labels_array
         return labels_array(self)
 
     def copy(self) -> 'Axis':
         r"""
-        Returns a copy of the axis.
+        Return a copy of the axis.
         """
         new_axis = Axis([], self.name)
         # XXX: I wonder if we should make a copy of the labels + mapping. There should at least be an option.
@@ -1122,7 +1122,7 @@ def copy(self) -> 'Axis':
 
     def replace(self, old, new=None) -> 'Axis':
         r"""
-        Returns a new axis with some labels replaced.
+        Return a new axis with some labels replaced.
 
         Parameters
         ----------
@@ -1168,7 +1168,7 @@ def replace(self, old, new=None) -> 'Axis':
 
     def apply(self, func) -> 'Axis':
         r"""
-        Returns a new axis with the labels transformed by func.
+        Return a new axis with the labels transformed by func.
 
         Parameters
         ----------
@@ -1191,7 +1191,7 @@ def apply(self, func) -> 'Axis':
     # XXX: rename to named like Group?
     def rename(self, name) -> 'Axis':
         r"""
-        Renames the axis.
+        Rename the axis.
 
         Parameters
         ----------
@@ -1219,7 +1219,7 @@ def rename(self, name) -> 'Axis':
     _rename = renamed_to(rename, '_rename', raise_error=True)
 
     def union(self, other) -> 'Axis':
-        r"""Returns axis with the union of this axis labels and other labels.
+        r"""Return axis with the union of this axis labels and other labels.
 
         Labels relative order will be kept intact, but only unique labels will be returned. Labels from this axis will
         be before labels from other.
@@ -1255,7 +1255,7 @@ def union(self, other) -> 'Axis':
         return Axis(unique_multi((self.labels, other)), self.name)
 
     def intersection(self, other) -> 'Axis':
-        r"""Returns axis with the (set) intersection of this axis labels and other labels.
+        r"""Return axis with the (set) intersection of this axis labels and other labels.
 
         In other words, this will use labels from this axis if they are also in other. Labels relative order will be
         kept intact.
@@ -1292,7 +1292,7 @@ def intersection(self, other) -> 'Axis':
         return Axis([label for label in self.labels if label in to_keep], self.name)
 
     def difference(self, other) -> 'Axis':
-        r"""Returns axis with the (set) difference of this axis labels and other labels.
+        r"""Return axis with the (set) difference of this axis labels and other labels.
 
         In other words, this will use labels from this axis if they are not in other. Labels relative order will be
         kept intact.
@@ -1383,7 +1383,7 @@ def align(self, other, join='outer') -> 'Axis':
 
     def to_hdf(self, filepath, key=None) -> None:
         r"""
-        Writes axis to a HDF file.
+        Write axis to a HDF file.
 
         A HDF file can contain multiple axes.
         The 'key' parameter is a unique identifier for the axis.
@@ -1440,7 +1440,7 @@ def dtype(self) -> np.dtype:
         return self._labels.dtype
 
     def ignore_labels(self) -> 'Axis':
-        r"""Returns a wildcard axis with the same name and length than this axis.
+        r"""Return a wildcard axis with the same name and length than this axis.
 
         Useful when you want to apply operations between two arrays with the same shape but incompatible axes
         (different labels).
@@ -1565,7 +1565,7 @@ def __iter__(self) -> Iterable[Axis]:
 
     # TODO: move a few doctests to unit tests
     def iter_labels(self, axes=None, ascending=True) -> Sequence[Tuple[IGroup, ...]]:
-        r"""Returns a view of the axes labels.
+        r"""Return a view of the axes labels.
 
         Parameters
         ----------
@@ -1630,7 +1630,7 @@ def iter_labels(self, axes=None, ascending=True) -> Sequence[Tuple[IGroup, ...]]
         return p
 
     def _combined_iflat(self, flat_key, sep='_'):
-        """return the combined axes AxisCollection indexed by flat_key
+        """Return the combined axes AxisCollection indexed by flat_key.
 
         Parameters
         ----------
@@ -1655,7 +1655,7 @@ def _combined_iflat(self, flat_key, sep='_'):
         return self._adv_keys_to_combined_axes(nd_key, sep=sep)
 
     def _iflat(self, key):
-        """Returns labels for each axis corresponding to indexing an array with the AxisCollection axes
+        """Return labels for each axis corresponding to indexing an array with the AxisCollection axes.
 
         Parameters
         ----------
@@ -1739,7 +1739,7 @@ def _ipython_key_completions_(self) -> List[str]:
     # It could either be moved to make_numpy_broadcastable or made non default
     def get_by_pos(self, key, i) -> Axis:
         r"""
-        Returns axis corresponding to a key, or to position i if the key has no name and key object not found.
+        Return axis corresponding to a key, or to position i if the key has no name and key object not found.
 
         Parameters
         ----------
@@ -1841,7 +1841,7 @@ def __radd__(self, other) -> 'AxisCollection':
 
     def __and__(self, other) -> 'AxisCollection':
         r"""
-        Returns the intersection of this collection and other.
+        Return the intersection of this collection and other.
         """
         if not isinstance(other, AxisCollection):
             other = AxisCollection(other)
@@ -1887,7 +1887,7 @@ def __contains__(self, key) -> bool:
 
     def isaxis(self, value) -> bool:
         r"""
-        Tests if input is an Axis object or the name of an axis contained in self.
+        Test if input is an Axis object or the name of an axis contained in self.
 
         Parameters
         ----------
@@ -1950,7 +1950,7 @@ def __repr__(self) -> str:
     # TODO: kill name argument (does not seem to be used anywhere
     def get(self, key, default=None, name=None) -> Axis:
         r"""
-        Returns axis corresponding to key. If not found, the argument `name` is used to create a new Axis.
+        Return axis corresponding to key. If not found, the argument `name` is used to create a new Axis.
         If `name` is None, the `default` axis is then returned.
 
         Parameters
@@ -1986,7 +1986,7 @@ def get(self, key, default=None, name=None) -> Axis:
 
     def get_all(self, key) -> 'AxisCollection':
         r"""
-        Returns all axes from key if present and length 1 wildcard axes otherwise.
+        Return all axes from key if present and length 1 wildcard axes otherwise.
 
         Parameters
         ----------
@@ -2032,7 +2032,7 @@ def get_pos_default(k, i):
 
     def keys(self) -> List[str]:
         r"""
-        Returns list of all axis names.
+        Return list of all axis names.
 
         Examples
         --------
@@ -2047,7 +2047,7 @@ def keys(self) -> List[str]:
 
     def pop(self, axis=-1) -> Axis:
         r"""
-        Removes and returns an axis.
+        Remove and return an axis.
 
         Parameters
         ----------
@@ -2081,7 +2081,7 @@ def pop(self, axis=-1) -> Axis:
 
     def append(self, axis) -> None:
         r"""
-        Appends axis at the end of the collection.
+        Append axis at the end of the collection.
 
         Parameters
         ----------
@@ -2106,7 +2106,7 @@ def append(self, axis) -> None:
 
     def check_compatible(self, axes) -> None:
         r"""
-        Checks if axes passed as argument are compatible with those contained in the collection.
+        Check if axes passed as argument are compatible with those contained in the collection.
         Raises ValueError if not.
 
         See Also
@@ -2127,7 +2127,7 @@ def check_compatible(self, axes) -> None:
     # TODO: deprecate replace_wildcards argument (unused)
     def extend(self, axes, validate=True, replace_wildcards=False) -> None:
         r"""
-        Extends the collection by appending the axes from `axes`.
+        Extend the collection by appending the axes from `axes`.
 
         Parameters
         ----------
@@ -2186,7 +2186,7 @@ def get_axis(col, i, axis) -> Optional[Axis]:
 
     def index(self, axis, compatible=False) -> int:
         r"""
-        Returns the index of axis.
+        Return the index of axis.
 
         `axis` can be a name or an Axis object (or an index). If the Axis object itself exists in the list, index()
         will return it. Otherwise, it will return the index of the local axis with the same name than the key (whether
@@ -2265,7 +2265,7 @@ def index(self, axis, compatible=False) -> int:
     # append, extend, pop, __delitem__, __setitem__)
     def insert(self, index, axis) -> None:
         r"""
-        Inserts axis before index.
+        Insert axis before index.
 
         Parameters
         ----------
@@ -2292,17 +2292,17 @@ def insert(self, index, axis) -> None:
 
     def copy(self) -> 'AxisCollection':
         r"""
-        Returns a copy.
+        Return a copy.
         """
         return self[:]
 
     def rename(self, renames=None, to=None, **kwargs) -> 'AxisCollection':
-        r"""Renames axes of the collection.
+        r"""Rename axes of the collection.
 
         Parameters
         ----------
         renames : axis ref or dict {axis ref: str} or list of tuple (axis ref, str), optional
-            Renames to apply. If a single axis reference is given, the `to` argument must be used.
+            Rename to apply. If a single axis reference is given, the `to` argument must be used.
         to : str or Axis, optional
             New name if `renames` contains a single axis reference.
         **kwargs : str or Axis
@@ -2461,7 +2461,7 @@ def replace(self, axes_to_replace=None, new_axis=None, inplace=False, **kwargs)
             return AxisCollection(axes)
 
     def set_labels(self, axis=None, labels=None, inplace=False, **kwargs) -> 'AxisCollection':
-        r"""Replaces the labels of one or several axes.
+        r"""Replace the labels of one or several axes.
 
         Parameters
         ----------
@@ -2605,7 +2605,7 @@ def set_labels(self, axis=None, labels=None, inplace=False, **kwargs) -> 'AxisCo
     # TODO: deprecate method (should use __sub__ instead)
     def without(self, axes) -> 'AxisCollection':
         r"""
-        Returns a new collection without some axes.
+        Return a new collection without some axes.
 
         You can use a comma separated list of names.
 
@@ -2688,7 +2688,7 @@ def _translate_nice_key(self, axis_key):
 
     def _guess_axis(self, axis_key):
         """
-        Translates any *single axis* key to an LGroup on the real axis.
+        Translate any *single axis* key to an LGroup on the real axis.
         """
         if isinstance(axis_key, Group):
             group_axis = axis_key.axis
@@ -2704,7 +2704,7 @@ def _guess_axis(self, axis_key):
 
     def _translate_axis_key_chunk(self, axis_key):
         """
-        Translates any *single axis* key to an (axis, indices) pair.
+        Translate any *single axis* key to an (axis, indices) pair.
 
         Parameters
         ----------
@@ -2758,7 +2758,7 @@ def _translate_axis_key_chunk(self, axis_key):
 
     def _translate_axis_key(self, axis_key):
         """
-        Translates single axis label-based key to (axis, indices)
+        Translate single axis label-based key to (axis, indices).
 
         Parameters
         ----------
@@ -2826,7 +2826,7 @@ def _translate_axis_key(self, axis_key):
 
     def _key_to_axis_indices_dict(self, key):
         """
-        Translates any label-based key to an {axis: indices} dict.
+        Translate any label-based key to an {axis: indices} dict.
 
         Parameters
         ----------
@@ -2894,7 +2894,7 @@ def _key_to_axis_indices_dict(self, key):
 
     def _key_to_raw_and_axes(self, key, collapse_slices=False, translate_key=True, points=False, wildcard=False):
         r"""
-        Transforms any key (from Array.__getitem__) to a raw numpy key, the resulting axes, and potentially a tuple
+        Transform any key (from Array.__getitem__) to a raw numpy key, the resulting axes, and potentially a tuple
         of indices to transpose axes back to where they were.
 
         Parameters
@@ -3065,7 +3065,7 @@ def slice_to_sequence(axis, axis_key):
     @property
     def labels(self) -> List[Iterable[Scalar]]:
         r"""
-        Returns the list of labels of the axes.
+        Return the list of labels of the axes.
 
         Returns
         -------
@@ -3085,7 +3085,7 @@ def labels(self) -> List[Iterable[Scalar]]:
     @property
     def names(self) -> List[str]:
         r"""
-        Returns the list of (raw) names of the axes.
+        Return the list of (raw) names of the axes.
 
         Returns
         -------
@@ -3126,7 +3126,7 @@ def axis_summary(axis, idx=None):
     @property
     def display_names(self) -> List[str]:
         r"""
-        Returns the list of (display) names of the axes.
+        Return the list of (display) names of the axes.
 
         Returns
         -------
@@ -3148,7 +3148,7 @@ def display_names(self) -> List[str]:
     @property
     def ids(self) -> List[Union[str, int]]:
         r"""
-        Returns the list of ids of the axes.
+        Return the list of ids of the axes.
 
         Returns
         -------
@@ -3172,7 +3172,7 @@ def ids(self) -> List[Union[str, int]]:
 
     def axis_id(self, axis) -> Union[str, int]:
         r"""
-        Returns the id of an axis.
+        Return the id of an axis.
 
         Returns
         -------
@@ -3198,7 +3198,7 @@ def axis_id(self, axis) -> Union[str, int]:
     @property
     def shape(self) -> Tuple[int, ...]:
         r"""
-        Returns the shape of the collection.
+        Return the shape of the collection.
 
         Returns
         -------
@@ -3219,7 +3219,7 @@ def shape(self) -> Tuple[int, ...]:
     @property
     def size(self) -> int:
         r"""
-        Returns the size of the collection, i.e.
+        Return the size of the collection, i.e.
         the number of elements of the array.
 
         Returns
@@ -3240,7 +3240,7 @@ def size(self) -> int:
     @property
     def info(self) -> str:
         r"""
-        Describes the collection (shape and labels for each axis).
+        Describe the collection (shape and labels for each axis).
 
         Returns
         -------
@@ -3381,7 +3381,7 @@ def combine_axes(self, axes=None, sep='_', wildcard=False, front_if_spread=False
         return new_axes
 
     def split_axes(self, axes=None, sep='_', names=None, regex=None) -> 'AxisCollection':
-        r"""Split axes and returns a new collection
+        r"""Split axes and returns a new collection.
 
         The split axes are inserted where the combined axis was.
 
@@ -3604,7 +3604,7 @@ def align(self, other, join='outer', axes=None) -> Tuple['AxisCollection', 'Axis
 
     # XXX: make this into a public method/property? AxisCollection.flat_labels[flat_indices]?
     def _flat_lookup(self, flat_indices):
-        r"""Return labels corresponding to indices into the flattened axes
+        r"""Return labels corresponding to indices into the flattened axes.
 
         Parameters
         ----------
@@ -3641,7 +3641,7 @@ def _flat_lookup(self, flat_indices):
 
     def _adv_keys_to_combined_axis_la_keys(self, key, wildcard=False, sep='_'):
         r"""
-        Returns key with the non-Array "advanced indexing" key parts transformed to Arrays with a combined axis.
+        Return key with the non-Array "advanced indexing" key parts transformed to Arrays with a combined axis.
         Scalar, slice and Array key parts are just left as is.
 
         Parameters
@@ -3669,7 +3669,7 @@ def _adv_keys_to_combined_axis_la_keys(self, key, wildcard=False, sep='_'):
 
     def _adv_keys_to_combined_axes(self, key, wildcard=False, sep='_'):
         r"""
-        Returns an AxisCollection corresponding to the combined axis of the non-Array "advanced indexing" key parts.
+        Return an AxisCollection corresponding to the combined axis of the non-Array "advanced indexing" key parts.
         Scalar, slice and Array key parts are ignored.
 
         Parameters
diff --git a/larray/core/checked.py b/larray/core/checked.py
index 75d718c0d..102dc199c 100644
--- a/larray/core/checked.py
+++ b/larray/core/checked.py
@@ -4,7 +4,7 @@
 
 import numpy as np
 
-from typing import TYPE_CHECKING, Type, Any, Dict, Set, List, no_type_check
+from typing import TYPE_CHECKING, Type, Any, Dict, Set, List, no_type_check, Optional
 
 from larray.core.axis import AxisCollection
 from larray.core.array import Array, full
@@ -134,7 +134,7 @@ def smart_deepcopy(obj: Obj) -> Obj:
         """
         Return type as is for immutable built-in types
         Use obj.copy() for built-in empty collections
-        Use copy.deepcopy() for non-empty collections and unknown objects
+        Use copy.deepcopy() for non-empty collections and unknown objects.
         """
         obj_type = obj.__class__
         if obj_type in IMMUTABLE_NON_COLLECTIONS_TYPES:
@@ -235,7 +235,7 @@ def __new__(mcs, name, bases, namespace, **kwargs):
 
     class CheckedSession(Session, AbstractCheckedSession, metaclass=ModelMetaclass):
         """
-        This class is intended to be inherited by user defined classes in which the variables of a model are declared.
+        Class intended to be inherited by user defined classes in which the variables of a model are declared.
         Each declared variable is constrained by a type defined explicitly or deduced from the given default value
         (see examples below).
         All classes inheriting from `CheckedSession` will have access to all methods of the :py:class:`Session` class.
@@ -398,6 +398,7 @@ class CheckedSession(Session, AbstractCheckedSession, metaclass=ModelMetaclass):
         dumping population ... done
         dumping undeclared_var ... done
         """
+
         if TYPE_CHECKING:
             # populated by the metaclass, defined here to help IDEs only
             __fields__: Dict[str, ModelField] = {}
@@ -433,7 +434,7 @@ def __init__(self, *args, meta=None, **kwargs):
             # create an intermediate Session object to not call the __setattr__
             # and __setitem__ overridden in the present class and in case a filepath
             # is given as only argument
-            # todo: refactor Session.load() to use a private function which returns the handler directly
+            # TODO: refactor Session.load() to use a private function which returns the handler directly
             # so that we can get the items out of it and avoid this
             input_data = dict(Session(*args, **kwargs))
 
@@ -501,7 +502,7 @@ def __getstate__(self) -> Dict[str, Any]:
         def __setstate__(self, state: Dict[str, Any]) -> None:
             object.__setattr__(self, '__dict__', state['__dict__'])
 
-        def dict(self, exclude: Set[str] = None) -> Dict[str, Any]:
+        def dict(self, exclude: Optional[Set[str]] = None) -> Dict[str, Any]:
             return {k: v for k, v in self.items() if k not in exclude}
 
     class CheckedParameters(CheckedSession):
@@ -555,6 +556,7 @@ class CheckedParameters(CheckedSession):
         TypeError: Cannot change the value of the variable 'variant_name' since 'Parameters'
         is immutable and does not support item assignment
         """
+
         class Config:
             # whether models are faux-immutable, i.e. whether __setattr__ is allowed.
             # (default: True)
diff --git a/larray/core/group.py b/larray/core/group.py
index 1fdde9256..dd0b8d61f 100644
--- a/larray/core/group.py
+++ b/larray/core/group.py
@@ -17,7 +17,7 @@
 
 def _slice_to_str(key: slice, repr_func=str) -> str:
     r"""
-    Converts a slice to a string
+    Convert a slice to a string.
 
     Examples
     --------
@@ -199,7 +199,7 @@ def generalized_range(start, stop, step=1) -> Union[range, List[str], List[int]]
 
 def _range_str_to_range(s, stack_depth=1) -> Union[range, List[str], List[int]]:
     r"""
-    Converts a range string to a range (of values).
+    Convert a range string to a range (of values).
     The end point is included.
 
     Parameters
@@ -345,7 +345,7 @@ def _seq_group_to_name(seq) -> Sequence[Any]:
 
 def _to_tick(v) -> Scalar:
     r"""
-    Converts any value to a tick (ie makes it hashable, and acceptable as an ndarray element)
+    Convert any value to a tick (ie makes it hashable, and acceptable as an ndarray element).
 
     scalar -> not modified
     slice -> 'start:stop'
@@ -387,7 +387,7 @@ def _to_tick(v) -> Scalar:
 
 def _to_ticks(s, parse_single_int=False) -> Iterable[Scalar]:
     r"""
-    Makes a (list of) value(s) usable as the collection of labels for an Axis (ie hashable).
+    Make a (list of) value(s) usable as the collection of labels for an Axis (ie hashable).
 
     Strip strings, split them on ',' and translate "range strings" to list of values **including the end point** !
 
@@ -451,7 +451,7 @@ def _to_ticks(s, parse_single_int=False) -> Iterable[Scalar]:
 
 def _seq_str_to_seq(s, stack_depth=1, parse_single_int=False) -> Union[slice, List[Scalar], range, Scalar]:
     r"""
-    Converts a sequence string to its sequence (or scalar)
+    Convert a sequence string to its sequence (or scalar).
 
     Parameters
     ----------
@@ -494,7 +494,7 @@ def to_seq(b, stack_depth=1):
 
 def _to_key(v, stack_depth=1, parse_single_int=False) -> Key:
     r"""
-    Converts a value to a key usable for indexing (slice object, list of values,...).
+    Convert a value to a key usable for indexing (slice object, list of values,...).
     Strings are split on ',' and stripped. Colons (:) are interpreted as slices.
 
     Parameters
@@ -595,7 +595,7 @@ def _to_key(v, stack_depth=1, parse_single_int=False) -> Key:
 
 def _to_keys(value, stack_depth=1) -> Union[Key, Tuple[Key]]:
     r"""
-    Converts a (collection of) group(s) to a structure usable for indexing.
+    Convert a (collection of) group(s) to a structure usable for indexing.
 
     'label' or ['l1', 'l2'] or [['l1', 'l2'], ['l3']]
 
@@ -680,7 +680,7 @@ def _translate_group_key_hdf(key) -> str:
 def union(*args) -> List[Any]:
     # TODO: add support for LGroup and lists
     r"""
-    Returns the union of several "value strings" as a list.
+    Return the union of several "value strings" as a list.
 
     Parameters
     ----------
@@ -715,6 +715,7 @@ class IGroupMaker:
     -----
     This class is used by the method `Axis.i`
     """
+
     __slots__ = ('axis',)
 
     def __init__(self, axis):
@@ -735,6 +736,7 @@ def __len__(self) -> int:
 class Group:
     """Abstract Group.
     """
+
     __slots__ = ('key', 'name', 'axis')
 
     format_string = None
@@ -779,7 +781,7 @@ def __repr__(self) -> str:
         else:
             if self.axis is not None:
                 # anonymous axis
-                axis_ref = f', axis={repr(self.axis)}'
+                axis_ref = f', axis={self.axis!r}'
             else:
                 axis_ref = ''
 
@@ -789,7 +791,7 @@ def __repr__(self) -> str:
                 key_repr = f'[{key_repr}]'
 
             s = f'{self.__class__.__name__}({key_repr}{axis_ref})'
-        return f"{s} >> {repr(self.name)}" if self.name is not None else s
+        return f"{s} >> {self.name!r}" if self.name is not None else s
 
     def __str__(self) -> str:
         return str(self.eval())
@@ -797,7 +799,7 @@ def __str__(self) -> str:
     # TODO: rename to "to_positional"
     def translate(self, bound=None, stop=False) -> Union[int, slice, Sequence[int]]:
         r"""
-        Translate key to a position if it is not already
+        Translate key to a position if it is not already.
 
         Parameters
         ----------
@@ -822,7 +824,7 @@ def eval(self) -> Union[Scalar, Sequence[Scalar]]:
 
     def to_label(self) -> Union[Scalar, slice, Sequence[Scalar]]:
         r"""
-        Translate key to labels, if it is not already
+        Translate key to labels, if it is not already.
 
         Returns
         -------
@@ -888,7 +890,7 @@ def __iter__(self) -> Iterable['LGroup']:
         return iter([LGroup(v, axis=axis) for v in self.eval()])
 
     def named(self, name) -> 'Group':
-        r"""Returns group with a different name.
+        r"""Return group with a different name.
 
         Parameters
         ----------
@@ -903,7 +905,7 @@ def named(self, name) -> 'Group':
     __rshift__ = named
 
     def with_axis(self, axis) -> 'Group':
-        r"""Returns group with a different axis.
+        r"""Return group with a different axis.
 
         Parameters
         ----------
@@ -1076,7 +1078,7 @@ def opmethod(self, other):
 
     def equals(self, other) -> bool:
         r"""
-        Checks if this group is equal to another group.
+        Check if this group is equal to another group.
         Two groups are equal if they have the same group and axis names and correspond to the same labels.
 
         Parameters
@@ -1138,7 +1140,7 @@ def equals(self, other) -> bool:
         return res if isinstance(res, bool) else all(res)
 
     def set(self) -> 'LSet':
-        r"""Creates LSet from this group
+        r"""Create LSet from this group.
 
         Returns
         -------
@@ -1147,7 +1149,7 @@ def set(self) -> 'LSet':
         return LSet(self.eval(), self.name, self.axis)
 
     def union(self, other) -> 'LSet':
-        r"""Returns (set) union of this label group and other.
+        r"""Return (set) union of this label group and other.
 
         Labels relative order will be kept intact, but only unique labels will be returned. Labels from this group will
         be before labels from other.
@@ -1175,7 +1177,7 @@ def union(self, other) -> 'LSet':
         return self.set().union(other)
 
     def intersection(self, other) -> 'LSet':
-        r"""Returns (set) intersection of this label group and other.
+        r"""Return (set) intersection of this label group and other.
 
         In other words, this will return labels from this group which are also in other. Labels relative order will be
         kept intact, but only unique labels will be returned.
@@ -1203,7 +1205,7 @@ def intersection(self, other) -> 'LSet':
         return self.set().intersection(other)
 
     def difference(self, other) -> 'LSet':
-        r"""Returns (set) difference of this label group and other.
+        r"""Return (set) difference of this label group and other.
 
         In other words, this will return labels from this group without those in other. Labels relative order will be
         kept intact, but only unique labels will be returned.
@@ -1237,7 +1239,7 @@ def __contains__(self, item) -> bool:
 
     def startingwith(self, prefix) -> 'LGroup':
         r"""
-        Returns a group with the labels starting with the specified string.
+        Return a group with the labels starting with the specified string.
 
         Parameters
         ----------
@@ -1265,7 +1267,7 @@ def startingwith(self, prefix) -> 'LGroup':
 
     def endingwith(self, suffix) -> 'LGroup':
         r"""
-        Returns a group with the labels ending with the specified string.
+        Return a group with the labels ending with the specified string.
 
         Parameters
         ----------
@@ -1293,7 +1295,7 @@ def endingwith(self, suffix) -> 'LGroup':
 
     def matching(self, deprecated=None, pattern=None, regex=None) -> 'LGroup':
         r"""
-        Returns a group with all the labels matching the specified pattern or regular expression.
+        Return a group with all the labels matching the specified pattern or regular expression.
 
         Parameters
         ----------
@@ -1362,7 +1364,7 @@ def matching(self, deprecated=None, pattern=None, regex=None) -> 'LGroup':
 
     def containing(self, substring) -> 'LGroup':
         r"""
-        Returns a group with all the labels containing the specified substring.
+        Return a group with all the labels containing the specified substring.
 
         Parameters
         ----------
@@ -1390,7 +1392,7 @@ def containing(self, substring) -> 'LGroup':
 
     def to_hdf(self, filepath, key=None, axis_key=None) -> None:
         r"""
-        Writes group to a HDF file.
+        Write group to a HDF file.
 
         A HDF file can contain multiple groups.
         The 'key' parameter is a unique identifier for the group.
@@ -1558,6 +1560,7 @@ class LGroup(Group):
     >>> teens
     X.age[10:19] >> 'teens'
     """
+
     __slots__ = ()
     format_string = "{axis}[{key}]"
 
@@ -1568,7 +1571,7 @@ def __init__(self, key, name=None, axis=None):
     # XXX: return IGroup instead?
     def translate(self, bound=None, stop=False) -> int:
         r"""
-        compute position(s) of group
+        compute position(s) of group.
         """
         if bound is None:
             bound = self.key
@@ -1618,6 +1621,7 @@ class LSet(LGroup):
     >>> abc & letters['b:d']
     letters['b', 'c'].set()
     """
+
     __slots__ = ()
     format_string = "{axis}[{key}].set()"
 
@@ -1681,12 +1685,13 @@ class IGroup(Group):
     axis : int, str, Axis, optional
         Axis for group.
     """
+
     __slots__ = ()
     format_string = "{axis}.i[{key}]"
 
     def translate(self, bound=None, stop=False) -> Union[int, slice, Sequence[int]]:
         r"""
-        compute position(s) of group
+        compute position(s) of group.
         """
         if bound is not None:
             return bound
diff --git a/larray/core/metadata.py b/larray/core/metadata.py
index 5239e5dc5..806691644 100644
--- a/larray/core/metadata.py
+++ b/larray/core/metadata.py
@@ -52,6 +52,7 @@ class Metadata(AttributeDict):
 
     >>> del arr.meta.creation_date
     """
+
     def __larray__(self):
         from larray.core.array import stack
         return stack(self.items(), axes='metadata')
@@ -66,9 +67,11 @@ def from_array(cls, array) -> 'Metadata':
         from pandas import to_numeric, to_datetime
 
         def _convert_value(value):
+            # errors='ignore' => the value is unmodified if it does not parse
             value = to_numeric([value], errors='ignore')[0]
             if isinstance(value, str):
-                value = to_datetime(value, errors='ignore', infer_datetime_format=True)
+                # same here
+                value = to_datetime(value, errors='ignore')
             return value
 
         return Metadata({key: _convert_value(value) for key, value in zip(array.axes.labels[0], array.data)})
diff --git a/larray/core/misc.py b/larray/core/misc.py
index 9b042b52c..160a8e08d 100644
--- a/larray/core/misc.py
+++ b/larray/core/misc.py
@@ -5,7 +5,7 @@
 
 def isscalar(element: Any) -> bool:
     r"""
-    Returns `True` if the type of element is a scalar type.
+    Return `True` if the type of element is a scalar type.
 
     Parameters
     ----------
diff --git a/larray/core/session.py b/larray/core/session.py
index d820fac29..57280ce74 100644
--- a/larray/core/session.py
+++ b/larray/core/session.py
@@ -76,6 +76,7 @@ class Session:
     title: my title
     author: John Smith
     """
+
     def __init__(self, *args, meta=None, **kwargs):
         object.__setattr__(self, '_objects', {})
         if meta is None:
@@ -102,7 +103,7 @@ def __init__(self, *args, meta=None, **kwargs):
 
     @property
     def meta(self) -> Metadata:
-        r"""Returns metadata of the session.
+        r"""Return metadata of the session.
 
         Returns
         -------
@@ -259,7 +260,7 @@ def __getitem__(self, key) -> Union[Any, 'Session']:
 
     def get(self, key, default=None) -> Any:
         r"""
-        Returns the object corresponding to the key.
+        Return the object corresponding to the key.
         If the key doesn't correspond to any object, a default one can be returned.
 
         Parameters
@@ -444,7 +445,7 @@ def _update_from_iterable(self, it) -> None:
 
     def save(self, fname, names=None, engine='auto', overwrite=True, display=False, **kwargs) -> None:
         r"""
-        Dumps objects from the current session to a file, or several .csv files.
+        Dump objects from the current session to a file, or several .csv files.
         The Excel and CSV formats only dump objects of Array type (plus metadata).
 
         Parameters
@@ -598,7 +599,7 @@ def to_globals(self, names=None, depth=0, warn=True, inplace=False) -> None:
 
     def to_pickle(self, fname, names=None, overwrite=True, display=False, **kwargs) -> None:
         r"""
-        Dumps objects from the current session to a file using pickle.
+        Dump objects from the current session to a file using pickle.
 
         WARNING: never load a pickle file (.pkl or .pickle) from an untrusted source, as it can lead to arbitrary code
         execution.
@@ -658,7 +659,7 @@ def to_pickle(self, fname, names=None, overwrite=True, display=False, **kwargs)
 
     def to_hdf(self, fname, names=None, overwrite=True, display=False, **kwargs) -> None:
         r"""
-        Dumps objects from the current session to an HDF file.
+        Dump objects from the current session to an HDF file.
 
         Parameters
         ----------
@@ -715,7 +716,7 @@ def to_hdf(self, fname, names=None, overwrite=True, display=False, **kwargs) ->
 
     def to_excel(self, fname, names=None, overwrite=True, display=False, **kwargs) -> None:
         r"""
-        Dumps Array objects from the current session to an Excel file.
+        Dump Array objects from the current session to an Excel file.
 
         Parameters
         ----------
@@ -772,7 +773,7 @@ def to_excel(self, fname, names=None, overwrite=True, display=False, **kwargs) -
 
     def to_csv(self, fname, names=None, display=False, **kwargs) -> None:
         r"""
-        Dumps Array objects from the current session to CSV files.
+        Dump Array objects from the current session to CSV files.
 
         Parameters
         ----------
@@ -827,7 +828,7 @@ def to_csv(self, fname, names=None, display=False, **kwargs) -> None:
 
     def filter(self, pattern=None, kind=None) -> 'Session':
         r"""
-        Returns a new session with objects which match some criteria.
+        Return a new session with objects which match some criteria.
 
         Parameters
         ----------
@@ -883,7 +884,7 @@ def filter(self, pattern=None, kind=None) -> 'Session':
     @property
     def names(self) -> List[str]:
         r"""
-        Returns the list of names of the objects in the session.
+        Return the list of names of the objects in the session.
         The list is sorted alphabetically and does not follow the internal order.
 
         Returns
@@ -911,14 +912,14 @@ def names(self) -> List[str]:
         return sorted(self._objects.keys())
 
     def copy(self) -> 'Session':
-        r"""Returns a copy of the session.
+        r"""Return a copy of the session.
         """
         # this actually *does* a copy of the internal mapping (the mapping is not reused-as is)
         return self.__class__(self._objects)
 
     def keys(self) -> KeysView[str]:
         r"""
-        Returns a view on the session's keys.
+        Return a view on the session's keys.
 
         Returns
         -------
@@ -946,7 +947,7 @@ def keys(self) -> KeysView[str]:
 
     def values(self) -> ValuesView[Any]:
         r"""
-        Returns a view on the session's values.
+        Return a view on the session's values.
 
         Returns
         -------
@@ -973,7 +974,7 @@ def values(self) -> ValuesView[Any]:
 
     def items(self) -> ItemsView[str, Any]:
         r"""
-        Returns a view of the session’s items ((key, value) pairs).
+        Return a view of the session's items ((key, value) pairs).
 
         Returns
         -------
@@ -1334,8 +1335,8 @@ def lenient_transpose(v, axes):
 
     def compact(self, display=False) -> 'Session':
         r"""
-        Detects and removes "useless" axes (ie axes for which values are constant over the whole axis) for all array
-        objects in session
+        Detect and remove "useless" axes (ie axes for which values are constant over the whole axis) for all array
+        objects in session.
 
         Parameters
         ----------
@@ -1375,7 +1376,7 @@ def apply(self, func, *args, kind=Array, **kwargs) -> 'Session':
         *args : any
             Any extra arguments are passed to the function
         kind : type or tuple of types, optional
-            Type(s) of elements `func` will be applied to. Other elements will be left intact. Use ´kind=object´ to
+            Type(s) of elements `func` will be applied to. Other elements will be left intact. Use `kind=object` to
             apply to all kinds of objects. Defaults to Array.
         **kwargs : any
             Any extra keyword arguments are passed to the function
@@ -1424,8 +1425,8 @@ def apply(self, func, *args, kind=Array, **kwargs) -> 'Session':
         return Session({k: func(v, *args, **kwargs) if isinstance(v, kind) else v for k, v in self.items()})
 
     def summary(self, template=None) -> str:
-        """
-        Returns a summary of the content of the session.
+        r"""
+        Return a summary of the content of the session.
 
         Parameters
         ----------
@@ -1475,11 +1476,11 @@ def summary(self, template=None) -> str:
         >>> def print_array(key, array):
         ...     axes_names = ', '.join(array.axes.display_names)
         ...     shape = ' x '.join(str(i) for i in array.shape)
-        ...     return f"{key} -> {axes_names} ({shape})\\n  title = {array.meta.title}\\n  dtype = {array.dtype}"
+        ...     return f"{key} -> {axes_names} ({shape})\n  title = {array.meta.title}\n  dtype = {array.dtype}"
         >>> template = {Axis:  "{key} -> {name} [{labels}] ({length})",
         ...             Group: "{key} -> {name}: {axis_name}{labels} ({length})",
         ...             Array: print_array,
-        ...             Metadata: "\\t{key} -> {value}"}
+        ...             Metadata: "\t{key} -> {value}"}
         >>> print(s.summary(template))   # doctest: +NORMALIZE_WHITESPACE
         Metadata:
             title -> my title
@@ -1537,7 +1538,7 @@ def display(k, v, is_metadata=False) -> str:
 
     @property
     def nbytes(self) -> str:
-        r"""Returns the memory in bytes consumed by the session.
+        r"""Return the memory in bytes consumed by the session.
 
         Returns
         -------
@@ -1560,7 +1561,7 @@ def nbytes(self) -> str:
 
     @property
     def memory_used(self) -> str:
-        r"""Returns the memory consumed by the session in human readable form.
+        r"""Return the memory consumed by the session in human readable form.
 
         Returns
         -------
@@ -1584,7 +1585,7 @@ def _exclude_private_vars(vars_dict: Dict[str, Any]) -> Dict[str, Any]:
 
 def local_arrays(depth=0, include_private=False, meta=None) -> Session:
     r"""
-    Returns a session containing all local arrays sorted in alphabetical order.
+    Return a session containing all local arrays sorted in alphabetical order.
 
     Parameters
     ----------
@@ -1609,7 +1610,7 @@ def local_arrays(depth=0, include_private=False, meta=None) -> Session:
 
 def global_arrays(depth=0, include_private=False, meta=None) -> Session:
     r"""
-    Returns a session containing all global arrays sorted in alphabetical order.
+    Return a session containing all global arrays sorted in alphabetical order.
 
     Parameters
     ----------
@@ -1634,7 +1635,7 @@ def global_arrays(depth=0, include_private=False, meta=None) -> Session:
 
 def arrays(depth=0, include_private=False, meta=None) -> Session:
     r"""
-    Returns a session containing all available arrays (whether they are defined in local or global variables) sorted in
+    Return a session containing all available arrays (whether they are defined in local or global variables) sorted in
     alphabetical order. Local arrays take precedence over global ones (if a name corresponds to both a local
     and a global variable, the local array will be returned).
 
diff --git a/larray/inout/common.py b/larray/inout/common.py
index b14e3a6c8..7898291b7 100644
--- a/larray/inout/common.py
+++ b/larray/inout/common.py
@@ -49,6 +49,7 @@ class FileHandler:
     fname : Path
         Filename.
     """
+
     def __init__(self, fname: Union[str, Path], overwrite_file: bool = False):
         assert fname is not None
         if isinstance(fname, str):
@@ -68,16 +69,16 @@ def _open_for_write(self):
 
     def item_types(self) -> Dict[str, str]:
         r"""
-        Return dict with type of each stored object
+        Return dict with type of each stored object.
         """
         raise NotImplementedError()
 
     def _read_item(self, key, type, *args, **kwargs):
-        r"""Read item"""
+        r"""Read item."""
         raise NotImplementedError()
 
     def _read_metadata(self) -> Metadata:
-        r"""Read metadata"""
+        r"""Read metadata."""
         raise NotImplementedError()
 
     def _dump_item(self, key, value, *args, **kwargs):
@@ -85,18 +86,18 @@ def _dump_item(self, key, value, *args, **kwargs):
         raise NotImplementedError()
 
     def _dump_metadata(self, metadata):
-        r"""Dump metadata"""
+        r"""Dump metadata."""
         raise NotImplementedError()
 
     def save(self):
         r"""
-        Saves items in file.
+        Save items in file.
         """
         pass
 
     def close(self):
         r"""
-        Closes file.
+        Close file.
         """
         raise NotImplementedError()
 
@@ -112,7 +113,7 @@ def _update_original_file(self):
 
     def read(self, keys, *args, display=False, ignore_exceptions=False, **kwargs) -> Tuple[Metadata, dict]:
         r"""
-        Reads file content (HDF, Excel, CSV, ...) and returns a dictionary containing loaded objects.
+        Read file content (HDF, Excel, CSV, ...) and returns a dictionary containing loaded objects.
 
         Parameters
         ----------
@@ -156,7 +157,7 @@ def read(self, keys, *args, display=False, ignore_exceptions=False, **kwargs) ->
 
     def dump(self, metadata, values, *args, display=False, **kwargs):
         r"""
-        Dumps objects corresponding to keys in file in HDF, Excel, CSV, ... format
+        Dump objects corresponding to keys in file in HDF, Excel, CSV, ... format.
 
         Parameters
         ----------
diff --git a/larray/inout/csv.py b/larray/inout/csv.py
index ebaf7ca30..e4acac94b 100644
--- a/larray/inout/csv.py
+++ b/larray/inout/csv.py
@@ -22,7 +22,7 @@
 def read_csv(filepath_or_buffer, nb_axes=None, index_col=None, sep=',', headersep=None, decimal='.', fill_value=nan,
              na=nan, sort_rows=False, sort_columns=False, wide=True, dialect='larray', **kwargs) -> Array:
     r"""
-    Reads csv file and returns an array with the contents.
+    Read csv file and returns an array with the contents.
 
     Parameters
     ----------
@@ -237,7 +237,7 @@ def read_tsv(filepath_or_buffer, **kwargs) -> Array:
 
 
 def read_eurostat(filepath_or_buffer, **kwargs) -> Array:
-    r"""Reads EUROSTAT TSV (tab-separated) file into an array.
+    r"""Read EUROSTAT TSV (tab-separated) file into an array.
 
     EUROSTAT TSV files are special because they use tabs as data separators but comas to separate headers.
 
diff --git a/larray/inout/excel.py b/larray/inout/excel.py
index 5e670e351..bb0e70400 100644
--- a/larray/inout/excel.py
+++ b/larray/inout/excel.py
@@ -34,7 +34,7 @@
 def read_excel(filepath, sheet=0, nb_axes=None, index_col=None, fill_value=nan, na=nan,
                sort_rows=False, sort_columns=False, wide=True, engine=None, range=slice(None), **kwargs) -> Array:
     r"""
-    Reads excel file from sheet name and returns an Array with the contents
+    Read excel file from sheet name and returns an Array with the contents.
 
     Parameters
     ----------
@@ -233,6 +233,7 @@ class PandasExcelHandler(FileHandler):
     r"""
     Handler for Excel files using Pandas.
     """
+
     def _open_for_read(self):
         self.handle = pd.ExcelFile(self.fname)
 
@@ -282,6 +283,7 @@ class XLWingsHandler(FileHandler):
     r"""
     Handler for Excel files using XLWings.
     """
+
     def _get_original_file_name(self):
         # for XLWingsHandler, no need to create a temporary file, the job is already done in the Workbook class
         pass
diff --git a/larray/inout/hdf.py b/larray/inout/hdf.py
index 145ee935a..5d6e94eb6 100644
--- a/larray/inout/hdf.py
+++ b/larray/inout/hdf.py
@@ -27,7 +27,7 @@ def _get_type_from_attrs(attrs):
 
 def read_hdf(filepath_or_buffer, key, fill_value=nan, na=nan, sort_rows=False, sort_columns=False,
              name=None, **kwargs) -> Array:
-    r"""Reads a scalar or an axis or group or array named key from a HDF5 file in filepath (path+name)
+    r"""Read a scalar or an axis or group or array named key from a HDF5 file in filepath (path+name).
 
     Parameters
     ----------
@@ -133,6 +133,7 @@ class PandasHDFHandler(FileHandler):
     r"""
     Handler for HDF5 files using Pandas.
     """
+
     def _open_for_read(self):
         self.handle = HDFStore(self.fname, mode='r')
 
diff --git a/larray/inout/misc.py b/larray/inout/misc.py
index 7090eba3a..2d7a4e45d 100644
--- a/larray/inout/misc.py
+++ b/larray/inout/misc.py
@@ -14,7 +14,7 @@
 def from_lists(data, nb_axes=None, index_col=None, fill_value=nan, sort_rows=False, sort_columns=False,
                wide=True) -> Array:
     r"""
-    initialize array from a list of lists (lines)
+    initialize array from a list of lists (lines).
 
     Parameters
     ----------
diff --git a/larray/inout/pandas.py b/larray/inout/pandas.py
index 9e18ca12a..3696350f5 100644
--- a/larray/inout/pandas.py
+++ b/larray/inout/pandas.py
@@ -42,7 +42,7 @@ def parse(s):
 
 def index_to_labels(idx, sort=True):
     r"""
-    Returns unique labels for each dimension.
+    Return unique labels for each dimension.
     """
     if isinstance(idx, pd.MultiIndex):
         if sort:
@@ -78,7 +78,7 @@ def cartesian_product_df(df, sort_rows=False, sort_columns=False, fill_value=nan
 
 def from_series(s, sort_rows=False, fill_value=nan, meta=None, **kwargs) -> Array:
     r"""
-    Converts Pandas Series into Array.
+    Convert Pandas Series into Array.
 
     Parameters
     ----------
@@ -147,7 +147,7 @@ def from_series(s, sort_rows=False, fill_value=nan, meta=None, **kwargs) -> Arra
 def from_frame(df, sort_rows=False, sort_columns=False, parse_header=False, unfold_last_axis_name=False,
                fill_value=nan, meta=None, cartesian_prod=True, **kwargs) -> Array:
     r"""
-    Converts Pandas DataFrame into Array.
+    Convert Pandas DataFrame into Array.
 
     Parameters
     ----------
@@ -256,11 +256,11 @@ def from_frame(df, sort_rows=False, sort_columns=False, parse_header=False, unfo
 
 def set_dataframe_index_by_position(df, index_col_indices):
     """
-    equivalent to Dataframe.set_index but with column indices, not column labels
+    equivalent to Dataframe.set_index but with column indices, not column labels.
 
     This is necessary to support creating an index from columns without a name or with duplicate names.
 
-    Returns a new Dataframe
+    Return a new Dataframe
     """
     if not isinstance(index_col_indices, list):
         index_col_indices = [index_col_indices]
diff --git a/larray/inout/sas.py b/larray/inout/sas.py
index a6fc9bc23..12c6d83c7 100644
--- a/larray/inout/sas.py
+++ b/larray/inout/sas.py
@@ -13,10 +13,10 @@
 def read_sas(filepath, nb_axes=None, index_col=None, fill_value=nan, na=nan, sort_rows=False, sort_columns=False,
              **kwargs) -> Array:
     r"""
-    Reads sas file and returns an Array with the contents
+    Read sas file and returns an Array with the contents
         nb_axes: number of axes of the output array
     or
-        index_col: Positions of columns for the n-1 first axes (ex. [0, 1, 2, 3])
+        index_col: Positions of columns for the n-1 first axes (ex. [0, 1, 2, 3]).
     """
     if not np.isnan(na):
         fill_value = na
diff --git a/larray/inout/session.py b/larray/inout/session.py
index 6a1857f16..371f9deb6 100644
--- a/larray/inout/session.py
+++ b/larray/inout/session.py
@@ -6,7 +6,7 @@
 
 
 def register_file_handler(engine, extensions=None):
-    r"""Class decorator to register new file handler class
+    r"""Class decorator to register new file handler class.
 
     Parameters
     ----------
diff --git a/larray/inout/stata.py b/larray/inout/stata.py
index 735aadcb2..6a3f06f6f 100644
--- a/larray/inout/stata.py
+++ b/larray/inout/stata.py
@@ -8,7 +8,7 @@
 
 def read_stata(filepath_or_buffer, index_col=None, sort_rows=False, sort_columns=False, **kwargs) -> Array:
     r"""
-    Reads Stata .dta file and returns an Array with the contents
+    Read Stata .dta file and returns an Array with the contents.
 
     Parameters
     ----------
diff --git a/larray/inout/xw_excel.py b/larray/inout/xw_excel.py
index 527204536..25c3760c6 100644
--- a/larray/inout/xw_excel.py
+++ b/larray/inout/xw_excel.py
@@ -325,7 +325,7 @@ def __repr__(self):
 
     def _fill_slice(s, length):
         r"""
-        replaces a slice None bounds by actual bounds.
+        Replace slice None bounds by actual bounds.
 
         Parameters
         ----------
@@ -650,7 +650,7 @@ def open_excel(filepath=None, overwrite_file=False, visible=None, silent=None, a
 """
 
 Workbook.sheet_names.__doc__ = r"""
-Returns the names of the Excel sheets.
+Return the names of the Excel sheets.
 
 Examples
 --------
@@ -669,8 +669,8 @@ def open_excel(filepath=None, overwrite_file=False, visible=None, silent=None, a
 Saves the Workbook.
 
 If a path is being provided, this works like SaveAs() in Excel.
-If no path is specified and if the file hasn’t been saved previously,
-it’s being saved in the current working directory with the current filename.
+If no path is specified and if the file hasn't been saved previously,
+it's being saved in the current working directory with the current filename.
 Existing files are overwritten without prompting.
 
 Parameters
diff --git a/larray/inout/xw_reporting.py b/larray/inout/xw_reporting.py
index 9cb6671a0..fe99f2910 100644
--- a/larray/inout/xw_reporting.py
+++ b/larray/inout/xw_reporting.py
@@ -38,6 +38,7 @@ def __init__(self, template_dir=None, template=None, graphs_per_row=1):
     def template_dir(self):
         r"""
         Set the path to the directory containing the Excel template files (with '.crtx' extension).
+
         This method is mainly useful if your template files are located in several directories,
         otherwise pass the template directory directly the ExcelReport constructor.
 
@@ -117,6 +118,7 @@ def template(self, template):
     def set_item_default_size(self, kind, width=None, height=None):
         r"""
         Override the default 'width' and 'height' values for the given kind of item.
+
         A new value must be provided at least for 'width' or 'height'.
 
         Parameters
@@ -170,6 +172,7 @@ def graphs_per_row(self, graphs_per_row):
 class AbstractReportSheet(AbstractReportItem):
     r"""
     Represents a sheet dedicated to contains only graphical items (title banners, graphs).
+
     See :py:obj:`ExcelReport` for use cases.
 
     Parameters
@@ -189,9 +192,11 @@ class AbstractReportSheet(AbstractReportItem):
     --------
     ExcelReport
     """
+
     def add_title(self, title, width=None, height=None, fontsize=11):
         r"""
         Add a title item to the current sheet.
+
         Note that the current method only add a new item to the list of items to be generated.
         The report Excel file is generated only when the :py:obj:`~ExcelReport.to_excel` is called.
 
@@ -226,6 +231,7 @@ def add_graph(self, data, title=None, template=None, width=None, height=None, mi
                   xticks_spacing=None, customize_func=None, customize_kwargs=None):
         r"""
         Add a graph item to the current sheet.
+
         Note that the current method only add a new item to the list of items to be generated.
         The report Excel file is generated only when the :py:obj:`~ExcelReport.to_excel` is called.
 
@@ -302,8 +308,10 @@ def add_graphs(self, array_per_title, axis_per_loop_variable, template=None, wid
                    graphs_per_row=1, min_y=None, max_y=None, xticks_spacing=None, customize_func=None,
                    customize_kwargs=None):
         r"""
-        Add multiple graph items to the current sheet. This method is mainly useful when multiple
-        graphs are generated by iterating over one or several axes of an array (see examples below).
+        Add multiple graph items to the current sheet.
+
+        This method is mainly useful when multiple graphs are generated by iterating over one or several axes of an
+        array (see examples below).
         The report Excel file is generated only when the :py:obj:`~ExcelReport.to_excel` is called.
 
         Parameters
@@ -467,6 +475,7 @@ class AbstractExcelReport(AbstractReportItem):
     def new_sheet(self, sheet_name):
         r"""
         Add a new empty output sheet.
+
         This sheet will contain only graphical elements, all data are exported
         to a dedicated separate sheet.
 
@@ -494,7 +503,7 @@ def new_sheet(self, sheet_name):
 
     def sheet_names(self):
         r"""
-        Returns the names of the output sheets.
+        Return the names of the output sheets.
 
         Examples
         --------
diff --git a/larray/random.py b/larray/random.py
index 928db5cee..c45455b16 100644
--- a/larray/random.py
+++ b/larray/random.py
@@ -382,7 +382,7 @@ def permutation(x, axis=0) -> Array:
 
 def choice(choices=None, axes=None, replace=True, p=None, meta=None) -> Array:
     r"""
-    Generates a random sample from given choices
+    Generate a random sample from given choices.
 
     Parameters
     ----------
diff --git a/larray/util/misc.py b/larray/util/misc.py
index 0476269e4..6578a294a 100644
--- a/larray/util/misc.py
+++ b/larray/util/misc.py
@@ -1,5 +1,5 @@
 """
-Misc tools
+Misc tools.
 """
 
 import __main__
@@ -61,12 +61,13 @@ def get_col_width(table, index):
 
 
 def longest_word(s):
-    """Return length of the longest word in the given string
+    """Return length of the longest word in the given string.
 
     Parameters
     ----------
     s : str
         string to check
+
     Returns
     -------
     int
@@ -95,8 +96,12 @@ def get_min_width(table, index):
 def table2str(table, missing, summarize=True, maxwidth=200, numedges='auto', sep='  ', cont='...',
               keepcols=0, precision=None):
     """
-    table is a list of lists
-    :type table: list of list
+    Convert list of list to string.
+
+    Parameters
+    ----------
+    table : list of list
+        input to convert.
     """
     if not table:
         return ''
@@ -164,7 +169,7 @@ def table2str(table, missing, summarize=True, maxwidth=200, numedges='auto', sep
 # copied from itertools recipes
 def unique(iterable):
     """
-    Yields all elements once, preserving order. Remember all elements ever seen.
+    Yield all elements once, preserving order. Remember all elements ever seen.
 
     >>> list(unique('AAAABBBCCDAABBB'))
     ['A', 'B', 'C', 'D']
@@ -179,7 +184,7 @@ def unique(iterable):
 
 def unique_list(iterable, res=None, seen=None):
     """
-    Returns a list of all unique elements, preserving order. Remember all elements ever seen.
+    Return a list of all unique elements, preserving order. Remember all elements ever seen.
 
     >>> unique_list('AAAABBBCCDAABBB')
     ['A', 'B', 'C', 'D']
@@ -199,7 +204,9 @@ def unique_list(iterable, res=None, seen=None):
 
 def unique_multi(iterable_of_iterables):
     """
-    Returns a list of all unique elements across multiple iterables. Elements of earlier iterables will come first.
+    Return a list of all unique elements across multiple iterables.
+
+    Elements of earlier iterables will come first.
     """
     seen = set()
     res = []
@@ -210,7 +217,7 @@ def unique_multi(iterable_of_iterables):
 
 def has_duplicates(iterable):
     """
-    Returns whether iterable contains any duplicated element.
+    Return whether iterable contains any duplicated element.
     """
     # using a dict is faster than using a set (at least for Python <= 3.9)
     seen = {}
@@ -284,7 +291,7 @@ def __repr__(self):
 
 
 def array_lookup(array, mapping):
-    """pass all elements of an np.ndarray through a mapping"""
+    """Pass all elements of an np.ndarray through a mapping."""
     array = np.asarray(array)
     # TODO: this must be cached in the Axis
     # TODO: range axes should be optimized (reuse Pandas 0.18 indexes)
@@ -307,7 +314,7 @@ def array_lookup(array, mapping):
 
 
 def array_lookup2(array, sorted_keys, sorted_values):
-    """pass all elements of an np.ndarray through a "mapping" """
+    """Pass all elements of an np.ndarray through a "mapping"."""
     if not len(array):
         return np.empty(0, dtype=sorted_values.dtype)
 
@@ -328,7 +335,7 @@ def array_lookup2(array, sorted_keys, sorted_values):
 
 
 def split_on_condition(seq, condition):
-    """splits an iterable into two lists depending on a condition
+    """Split an iterable into two lists depending on a condition.
 
     Parameters
     ----------
@@ -352,7 +359,7 @@ def split_on_condition(seq, condition):
 
 
 def split_on_values(seq, values):
-    """splits an iterable into two lists depending on a list of values
+    """Split an iterable into two lists depending on a list of values.
 
     Parameters
     ----------
@@ -383,8 +390,9 @@ def notacomment(v):
 
 def strip_rows(lines):
     """
-    returns an iterator of lines with trailing blank (empty or
-    which contain only space) cells.
+    Return an iterator of lines without trailing blank cells.
+
+    Empty cells or cells containing only space are considered blank.
     """
     def isblank(s):
         return s == '' or s.isspace()
@@ -395,6 +403,10 @@ def isblank(s):
 
 def size2str(value):
     """
+    Convert number of bytes to a size string.
+
+    Examples
+    --------
     >>> size2str(0)
     '0 bytes'
     >>> size2str(100)
@@ -418,6 +430,7 @@ def size2str(value):
 
 def find_closing_chr(s, start=0):
     """
+    Find the position of character which matches/closes to first character of string s.
 
     Parameters
     ----------
@@ -507,6 +520,7 @@ def error_handler(error, flag):
 def _isintstring(s):
     """
     Return True if the passed string represents an integer.
+
     Zero padded integers are considered as strings and not integers.
 
     Parameters
@@ -532,8 +546,9 @@ def isposint(s):
 
 
 def _parse_bound(s, stack_depth=1, parse_int=True):
-    """Parse a string representing a single value, converting int-like strings to integers and evaluating expressions
-    within {}.
+    """Parse a string representing a single value.
+
+    It converts int-like strings to integers and evaluates expressions within {}.
 
     Parameters
     ----------
@@ -574,14 +589,14 @@ def _parse_bound(s, stack_depth=1, parse_int=True):
 
 def _isnoneslice(v):
     """
-    Checks if input is slice(None) object.
+    Check if input is slice(None) object.
     """
     return isinstance(v, slice) and v.start is None and v.stop is None and v.step is None
 
 
 def _seq_summary(seq, n=3, repr_func=repr, sep=' '):
     """
-    Returns a string representing a sequence by showing only the n first and last elements.
+    Return a string representing a sequence by showing only the n first and last elements.
 
     Examples
     --------
@@ -597,7 +612,7 @@ def _seq_summary(seq, n=3, repr_func=repr, sep=' '):
 
 def index_by_id(seq, value):
     """
-    Returns position of an object in a sequence.
+    Return position of an object in a sequence.
 
     Raises an error if the object is not in the list.
 
@@ -678,9 +693,11 @@ def wrapper(*args, **kwargs) -> R:
 
 class lazy_attribute:
     """
-    Decorate a method of a class and turn it into an attribute on the instance
-    when first called. Should obviously only be used when the result of the method is constant.
+    Decorate a method of a class to turn it into an instance attribute when first called.
+
+    Should obviously only be used when the result of the method is constant.
     """
+
     def __init__(self, func):
         self.func = func
         self.__doc__ = func.__doc__
@@ -721,7 +738,7 @@ def inverseop(opname):
 
 def np_array_common_dtype(arrays) -> np.dtype:
     """
-    Returns a dtype that all input numpy arrays can be safely cast to.
+    Return a dtype that all input numpy arrays can be safely cast to.
 
     Parameters
     ----------
@@ -743,7 +760,7 @@ def np_array_common_dtype(arrays) -> np.dtype:
     if any(mk != meta_kinds[0] for mk in meta_kinds[1:]):
         return np.dtype(object)
     elif meta_kinds[0] == 'numeric':
-        return np.find_common_type(dtypes, [])
+        return np.result_type(*dtypes)
     elif meta_kinds[0] == 'str':
         need_unicode = any(dt.kind == 'U' for dt in dtypes)
         # unicode are coded with 4 bytes
@@ -756,7 +773,7 @@ def np_array_common_dtype(arrays) -> np.dtype:
 
 def common_dtype(arrays) -> np.dtype:
     """
-    Returns a dtype that all input arrays can be safely cast to.
+    Return a dtype that all input arrays can be safely cast to.
 
     Parameters
     ----------
@@ -777,7 +794,8 @@ def common_dtype(arrays) -> np.dtype:
 
 
 class LHDFStore:
-    """Context manager for pandas HDFStore"""
+    """Context manager for pandas HDFStore."""
+
     def __init__(self, filepath_or_buffer, **kwargs):
         if isinstance(filepath_or_buffer, pd.HDFStore):
             if not filepath_or_buffer.is_open:
@@ -824,6 +842,7 @@ class SequenceZip:
     >>> list(z[1:4])
     [('b', 2), ('c', 3)]
     """
+
     def __init__(self, sequences):
         self.sequences = sequences
         length = len(sequences[0])
@@ -853,7 +872,8 @@ def __repr__(self):
 
 class Repeater:
     """
-    Returns a virtual sequence with value repeated n times.
+    Return a virtual sequence with value repeated n times.
+
     The sequence is never actually created in memory.
 
     Parameters
@@ -898,6 +918,7 @@ class Repeater:
     >>> list(r[10:])
     []
     """
+
     def __init__(self, value, n):
         self.value = value
         self.n = n
@@ -958,6 +979,7 @@ class Product:
     >>> list(Product([]))
     [()]
     """
+
     def __init__(self, sequences):
         self.sequences = sequences
         shape = [len(a) for a in self.sequences]
@@ -1005,7 +1027,7 @@ def _kill_np_type(value):
 
 def ensure_no_numpy_type(array):
     """
-    Converts array to a (potentially nested) list of builtin Python values (i.e. using no numpy-specific types)
+    Convert array to a (potentially nested) list of builtin Python values (i.e. using no numpy-specific types).
 
     Parameters
     ----------
@@ -1043,12 +1065,12 @@ def argsort(seq):
 
 
 def exactly_one(a: bool, b: bool, c: bool = False) -> bool:
-    """returns True if exactly one of a, b or c boolean arguments is True, False otherwise"""
+    """Return True if exactly one of a, b or c boolean arguments is True, False otherwise."""
     return (a or b) and not (a and b) if not c else not (a or b)
 
 
 def concatenate_ndarrays(arrays) -> np.ndarray:
-    """concatenate Sequence of np.ndarray, converting to object dtype if needed"""
+    """Concatenate Sequence of np.ndarray, converting to object dtype if needed."""
     dtype = np_array_common_dtype(arrays)
     if dtype.kind == 'O':
         # astype always copies, while asarray only copies if necessary
diff --git a/larray/viewer/__init__.py b/larray/viewer/__init__.py
index 21d2bef2a..d15b0b269 100644
--- a/larray/viewer/__init__.py
+++ b/larray/viewer/__init__.py
@@ -1,7 +1,7 @@
 
 def view(obj=None, title='', depth=0):
     r"""
-    Opens a new viewer window. Arrays are loaded in readonly mode and their content cannot be modified.
+    Open a new viewer window. Arrays are loaded in readonly mode and their content cannot be modified.
 
     Parameters
     ----------
@@ -34,7 +34,7 @@ def view(obj=None, title='', depth=0):
 
 def edit(obj=None, title='', minvalue=None, maxvalue=None, readonly=False, depth=0):
     r"""
-    Opens a new editor window.
+    Open a new editor window.
 
     Parameters
     ----------
@@ -75,7 +75,7 @@ def edit(obj=None, title='', minvalue=None, maxvalue=None, readonly=False, depth
 
 def debug(depth=0):
     r"""
-    Opens a new debug window.
+    Open a new debug window.
 
     Parameters
     ----------
@@ -92,7 +92,7 @@ def debug(depth=0):
 
 def compare(*args, depth=0, **kwargs):
     r"""
-    Opens a new comparator window, comparing arrays or sessions.
+    Open a new comparator window, comparing arrays or sessions.
 
     Parameters
     ----------
diff --git a/make_release.py b/make_release.py
index 12671f92c..cc0ef6460 100644
--- a/make_release.py
+++ b/make_release.py
@@ -12,6 +12,7 @@
 from subprocess import check_call
 
 from releaser import make_release, update_feedstock, short, chdir, insert_step_func, yes
+from releaser.utils import prepare_outlook_email
 from releaser.make_release import steps_funcs as make_release_steps
 from releaser.update_feedstock import steps_funcs as update_feedstock_steps
 
@@ -21,7 +22,8 @@
 SRC_CODE = "larray"
 SRC_DOC = join('doc', 'source')
 GITHUB_REP = "https://github.com/larray-project/larray"
-CONDA_FEEDSTOCK_REP = "https://github.com/larray-project/larray-feedstock.git"
+UPSTREAM_CONDAFORGE_FEEDSTOCK_REP = "https://github.com/conda-forge/larray-feedstock.git"
+ORIGIN_CONDAFORGE_FEEDSTOCK_REP = "https://github.com/larray-project/larray-feedstock.git"
 CONDA_BUILD_ARGS = {'--user': 'larray-project'}
 
 LARRAY_READTHEDOCS = "http://larray.readthedocs.io/en/stable/"
@@ -78,13 +80,11 @@ def merge_changelogs(build_dir, src_documentation, release_name, public_release,
 
 # TODO : move to larrayenv project
 def announce_new_release(release_name):
-    import win32com.client as win32
-    version = short(release_name)
-    outlook = win32.Dispatch('outlook.application')
-    mail = outlook.CreateItem(0)
-    mail.To = LARRAY_ANNOUNCE_MAILING_LIST
-    mail.Subject = f"LArray {version} released"
-    mail.Body = f"""\
+    version=short(release_name)
+    prepare_outlook_email(
+        to=LARRAY_ANNOUNCE_MAILING_LIST,
+        subject=f"LArray {version} released",
+        body = f"""\
 Hello all, 
 
 We are pleased to announce that version {version} of LArray is now available. 
@@ -96,8 +96,7 @@ def announce_new_release(release_name):
 
 As always, *any* feedback is very welcome, preferably on the larray-users 
 mailing list: {LARRAY_USERS_GROUP} (you need to register to be able to post).
-"""
-    mail.Display(True)
+""")
 
 
 if __name__ == '__main__':
@@ -130,7 +129,8 @@ def announce_new_release(release_name):
         if yes("Is metapackage larrayenv updated?", default='n'):
             announce_new_release(argv[2])
     elif argv[1] == '-c' or argv[1] == '--conda':
-        update_feedstock(GITHUB_REP, CONDA_FEEDSTOCK_REP, SRC_CODE, *argv[2:], tmp_dir=TMP_PATH_CONDA)
+        update_feedstock(GITHUB_REP, UPSTREAM_CONDAFORGE_FEEDSTOCK_REP, ORIGIN_CONDAFORGE_FEEDSTOCK_REP,
+                         SRC_CODE, *argv[2:], tmp_dir=TMP_PATH_CONDA)
     else:
         make_release(local_repository, PACKAGE_NAME, SRC_CODE, *argv[1:], src_documentation=SRC_DOC, tmp_dir=TMP_PATH,
                      conda_build_args=CONDA_BUILD_ARGS)
diff --git a/readthedocs.yml b/readthedocs.yml
index b97f1850d..84cc6a5c4 100644
--- a/readthedocs.yml
+++ b/readthedocs.yml
@@ -9,26 +9,26 @@ version: 2
 build:
   os: ubuntu-22.04
   tools:
-    python: "3.10"
+    python: "3.11"
 
 # Build documentation in the doc/ directory with Sphinx
 sphinx:
-    configuration: doc/source/conf.py
+  configuration: doc/source/conf.py
 
 # Optionally build your docs in additional formats such as PDF and ePub
 formats:
-    - pdf
+  - pdf
 
 # we do not use conda anymore because it consumes too much memory and thus crashes our builds
 # see:
 # - https://docs.readthedocs.io/en/stable/guides/build-using-too-many-resources.html#use-pip-when-possible
 # - https://github.com/readthedocs/readthedocs.org/issues/6692
 #conda:
-#    environment: doc/environment.yml
+#  environment: doc/environment.yml
 
 # Optionally declare the Python requirements required to build your docs
 python:
-    install:
-        - requirements: doc/requirements.txt
-        - method: pip
-          path: .
+  install:
+    - requirements: doc/requirements.txt
+    - method: pip
+      path: .
diff --git a/setup.py b/setup.py
index 77e035a5e..4d247849d 100644
--- a/setup.py
+++ b/setup.py
@@ -7,11 +7,12 @@ def readlocal(fname):
 
 
 DISTNAME = 'larray'
-VERSION = '0.34'
+VERSION = '0.34.1-dev'
 AUTHOR = 'Gaetan de Menten, Geert Bryon, Johan Duyck, Alix Damman'
 AUTHOR_EMAIL = 'gdementen@gmail.com'
 DESCRIPTION = "N-D labeled arrays in Python"
 LONG_DESCRIPTION = readlocal("README.rst")
+LONG_DESCRIPTION_CONTENT_TYPE = "text/x-rst"
 SETUP_REQUIRES = []
 # - pandas >= 0.20.0 is required since commit 01669f2024a7bffe47cceec0a0fd845f71b6f7cc
 #   (issue 702 : fixed bug when writing metadata using HDF format)
@@ -29,10 +30,10 @@ def readlocal(fname):
     'Intended Audience :: Developers',
     'Programming Language :: Python',
     'Programming Language :: Python :: 3',
-    'Programming Language :: Python :: 3.7',
     'Programming Language :: Python :: 3.8',
     'Programming Language :: Python :: 3.9',
     'Programming Language :: Python :: 3.10',
+    'Programming Language :: Python :: 3.11',
     'Topic :: Scientific/Engineering',
     'Topic :: Software Development :: Libraries',
 ]
@@ -46,6 +47,7 @@ def readlocal(fname):
     classifiers=CLASSIFIERS,
     description=DESCRIPTION,
     long_description=LONG_DESCRIPTION,
+    long_description_content_type=LONG_DESCRIPTION_CONTENT_TYPE,
     setup_requires=SETUP_REQUIRES,
     install_requires=INSTALL_REQUIRES,
     tests_require=TESTS_REQUIRE,


Test patch:
diff --git a/larray/tests/test_array.py b/larray/tests/test_array.py
index c90c80853..c24d2c720 100644
--- a/larray/tests/test_array.py
+++ b/larray/tests/test_array.py
@@ -1336,7 +1336,7 @@ def test_ipoints_indexer_setitem():
 
 def test_setitem_larray(array, small_array):
     """
-    tests Array.__setitem__(key, value) where value is an Array
+    Test Array.__setitem__(key, value) where value is an Array.
     """
     a, b, c, d = array.axes
 
@@ -1369,14 +1369,24 @@ def test_setitem_larray(array, small_array):
     raw[[1, 5, 9]] = raw[[1, 5, 9]] + 26.0
     assert_nparray_equal(arr.data, raw)
 
-    # d) value has the same axes than target but one has length 1
+    # d) value is an Array with a length-1 axis but the target region is a single cell
+    # these two cases raise a deprecation warning with Numpy 1.25+ (and will stop working
+    # in a future version), so we do not support that anymore (see issue #1070)
+    # res = ndtest((2, 3))
+    # res['a0', 'b1'] = Array([42])
+    # res['a0', 'b1'] = Array([42], 'dummy=d0')
+    # assert_larray_equal(res, from_string(r"""a\b b0  b1 b2
+    #                                           a0  0  42  2
+    #                                           a1  3   4  5"""))
+
+    # e) value has the same axes than target but one has length 1
     # arr = array.copy()
     # raw = array.data.copy()
     # raw[[1, 5, 9]] = np.sum(raw[[1, 5, 9]], axis=1, keepdims=True)
     # arr[as1_5_9] = arr[as1_5_9].sum(b=(b.all(),))
     # assert_nparray_equal(arr.data, raw)
 
-    # e) value has a missing dimension
+    # f) value has a missing dimension
     arr = array.copy()
     raw = array.data.copy()
     arr[as1_5_9] = arr[as1_5_9].sum(b)
@@ -1484,7 +1494,7 @@ def test_setitem_larray(array, small_array):
 
 def test_setitem_ndarray(array):
     """
-    tests Array.__setitem__(key, value) where value is a raw ndarray.
+    Test Array.__setitem__(key, value) where value is a raw ndarray.
     In that case, value.shape is more restricted as we rely on numpy broadcasting.
     """
     # a) value has exactly the same shape as the target slice
@@ -1506,7 +1516,7 @@ def test_setitem_ndarray(array):
 
 def test_setitem_scalar(array):
     """
-    tests Array.__setitem__(key, value) where value is a scalar
+    Test Array.__setitem__(key, value) where value is a scalar.
     """
     # a) list key (one dimension)
     arr = array.copy()
@@ -2780,6 +2790,18 @@ def test_binary_ops(small_array):
     res = arr + arr.a
     assert_larray_equal(res, arr + asarray(arr.a))
 
+    # Array + <unsupported type>
+    with must_raise(TypeError, "unsupported operand type(s) for +: 'Array' and 'object'"):
+        res = arr + object()
+
+    # Array + <unsupported type which implements the reverse op>
+    class Test:
+        def __radd__(self, other):
+            return "success"
+
+    res = arr + Test()
+    assert res == 'success'
+
 
 def test_binary_ops_no_name_axes(small_array):
     raw = small_array.data
@@ -2871,6 +2893,43 @@ def test_binary_ops_with_scalar_group():
     assert_larray_equal(arr + time.i[0], expected)
 
 
+def test_comparison_ops():
+    # simple array equality (identity)
+    a = Axis('a=a0,a1,a2')
+    arr = ndtest(a)
+    res = arr == arr
+    expected = ones(a)
+    assert_larray_equal(res, expected)
+
+    # simple array equality
+    arr = ndtest(a)
+    res = arr == zeros(a)
+    expected = Array([True, False, False], a)
+    assert_larray_equal(res, expected)
+
+    # invalid types
+    # a) eq
+    arr = ndtest(3)
+    res = arr == object()
+    assert res is False
+
+    # b) ne
+    res = arr != object()
+    assert res is True
+
+    # c) others
+    with must_raise(TypeError, "'>' not supported between instances of 'Array' and 'object'"):
+        res = arr > object()
+
+    # d) other type implementing the reverse comparison
+    class Test:
+        def __lt__(self, other):
+            return "success"
+
+    res = arr > Test()
+    assert res == 'success'
+
+
 def test_unary_ops(small_array):
     raw = small_array.data
 
@@ -3761,6 +3820,24 @@ def test_from_lists():
     assert_larray_equal(sorted_arr, expected)
 
 
+def test_to_series():
+    # simple test
+    arr = ndtest(3, dtype=np.int32)
+    res = arr.to_series()
+    expected = pd.Series([0, 1, 2], dtype="int32", index=pd.Index(['a0', 'a1', 'a2'], name='a'))
+    assert res.equals(expected)
+
+    # test for issue #1061 (object dtype labels array produce warning with Pandas1.4+)
+    # We use an explicit int64 type because for some reason, under Linux, summing an int32 array
+    # results in an int64 value, so it is easier to just use a int64 array in the first place so
+    # that the test works on both Windows and Linux
+    arr = ndtest("a=1..3", dtype=np.int64).with_total()[:3]
+    res = arr.to_series()
+    index = pd.Index([1, 2, 3], dtype=object, name='a')
+    expected = pd.Series([0, 1, 2], dtype="int64", index=index)
+    assert res.equals(expected)
+
+
 def test_from_series():
     # Series with Index as index
     expected = ndtest(3)
@@ -3795,6 +3872,27 @@ def test_from_series():
     assert_larray_equal(res, expected)
 
 
+def test_to_frame():
+    # these tests are for issue #1061
+    arr = ndtest("a=0..2").with_total()[:2]
+    df = arr.to_frame()
+    assert df.columns.name == 'a'
+    assert df.columns.to_list() == [0, 1, 2]
+
+    arr = ndtest("a=0..2;b=b0").with_total('a')[:2]
+    df = arr.to_frame()
+    assert df.columns.name == 'b'
+    assert df.columns.to_list() == ['b0']
+    assert df.index.name == 'a'
+    assert df.index.to_list() == [0, 1, 2]
+
+    arr = ndtest("a=0..2;b=b0;c=c0").with_total('a')[:2]
+    df = arr.to_frame()
+    assert df.columns.name == 'c'
+    assert df.columns.to_list() == ['c0']
+    assert df.index.names == ['a', 'b']
+
+
 def test_from_frame():
     # 1) data = scalar
     # ================
diff --git a/larray/tests/test_session.py b/larray/tests/test_session.py
index 67b6671a3..4ed80b9ec 100644
--- a/larray/tests/test_session.py
+++ b/larray/tests/test_session.py
@@ -8,7 +8,7 @@
 import pytest
 
 from larray.tests.common import meta
-from larray.tests.common import (assert_array_nan_equal, inputpath,
+from larray.tests.common import (assert_larray_equal, assert_array_nan_equal, inputpath,
                                  needs_xlwings, needs_pytables, needs_openpyxl, must_warn, must_raise)
 from larray.inout.common import _supported_scalars_types
 from larray import (Session, Axis, Array, Group, isnan, zeros_like, ndtest, ones_like,
@@ -656,5 +656,38 @@ def test_arrays():
     assert s.equals(s_expected)
 
 
+def test_stack():
+    # stacking all arrays of a single session
+    # =======================================
+    # a) using explicit axis
+    s = Session(arr1=ndtest(3), arr2=ndtest(3) + 10)
+    axis = Axis("array=arr2,arr1")
+    res = stack(s, axis)
+    expected = stack(arr1=s.arr1, arr2=s.arr2, axes=axis)
+    assert_larray_equal(res, expected)
+
+    # b) using explicit axis name
+    s = Session(arr1=ndtest(3), arr2=ndtest(3) + 10)
+    res = stack(s, "array")
+    expected = stack(arr1=s.arr1, arr2=s.arr2, axes="array")
+    assert_larray_equal(res, expected)
+
+    # c) using not axis information
+    s = Session(arr1=ndtest(3), arr2=ndtest(3) + 10)
+    res = stack(s)
+    expected = stack(arr1=s.arr1, arr2=s.arr2)
+    assert_larray_equal(res, expected)
+
+    # stacking two sessions (it will stack all arrays with corresponding names
+    s1 = Session(arr1=ndtest(3), arr2=ndtest(3) + 10)
+    s2 = Session(arr1=ndtest(3), arr2=ndtest(3) + 30)
+    expected_arr1 = stack(s1=s1.arr1, s2=s2.arr1, axes="session")
+    expected_arr2 = stack(s1=s1.arr2, s2=s2.arr2, axes="session")
+    res = stack(s1=s1, s2=s2, axes="session")
+    assert isinstance(res, Session)
+    assert_larray_equal(res.arr1, expected_arr1)
+    assert_larray_equal(res.arr2, expected_arr2)
+
+
 if __name__ == "__main__":
     pytest.main()