diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index a6610b0295a65bf470341e088b673963192a1a56..b7d75da736137b1a5a41a2a8f9c0ed2f503b9694 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -83,6 +83,7 @@ pages:
   allow_failure: false
   before_script:
     - pip install .
+    - cd tests
   variables:
     OTB_ROOT: /opt/otb
     LD_LIBRARY_PATH: /opt/otb/lib
@@ -97,12 +98,45 @@ import_pyotb:
 compute_ndvi:
   extends: .test_base
   script:
-    - cd tests
     - python3 ndvi_test.py
 
-pipeline_test:
+pipeline_test_shape:
   extends: .test_base
   script:
-    - cd tests
-    - python3 pipeline_test.py
-    - python3 pipeline_test.py backward
+    - python3 pipeline_test.py shape
+
+pipeline_test_shape_backward:
+  extends: .test_base
+  script:
+    - python3 pipeline_test.py shape backward
+
+pipeline_test_shape_nointermediate:
+  extends: .test_base
+  script:
+    - python3 pipeline_test.py shape no-intermediate-result
+
+pipeline_test_shape_backward_nointermediate:
+  extends: .test_base
+  script:
+    - python3 pipeline_test.py shape backward no-intermediate-result
+
+pipeline_test_write:
+  extends: .test_base
+  script:
+    - python3 pipeline_test.py write
+
+pipeline_test_write_backward:
+  extends: .test_base
+  script:
+    - python3 pipeline_test.py write backward
+
+pipeline_test_write_nointermediate:
+  extends: .test_base
+  script:
+    - python3 pipeline_test.py write no-intermediate-result
+
+pipeline_test_write_backward_nointermediate:
+  extends: .test_base
+  script:
+    - python3 pipeline_test.py write backward no-intermediate-result
+
diff --git a/README.md b/README.md
index 9879d45849f52e69c2d3a72a74f0fa0e5cae23a2..6f0adcd07969ae3eb2d46d78113cb1f364ec6a68 100644
--- a/README.md
+++ b/README.md
@@ -11,7 +11,7 @@ Requirements:
 pip install pyotb --upgrade
 ```
 
-For Python>=3.6, latest version available is pyotb 1.4.0 For Python 3.5, latest version available is pyotb 1.2.2
+For Python>=3.6, latest version available is pyotb 1.4.1 For Python 3.5, latest version available is pyotb 1.2.2
 
 ## Quickstart: running an OTB application as a oneliner
 pyotb has been written so that it is more convenient to run an application in Python.
diff --git a/doc/index.md b/doc/index.md
index eb936d5f7bee40fe30238214e817770a4576c66b..efebb2952b8a6b8f7d43252af6736722464b325f 100644
--- a/doc/index.md
+++ b/doc/index.md
@@ -1,4 +1,4 @@
-# Pyotb
+# Pyotb: Orfeo Toolbox for Python
 
 pyotb is a Python extension of Orfeo Toolbox. It has been built on top of the existing Python API of OTB, in order 
 to make OTB more Python friendly.
@@ -20,4 +20,7 @@ to make OTB more Python friendly.
 - [Comparison between pyotb and OTB native library](comparison_otb.md)
 - [OTB versions](otb_versions.md)
 - [Managing loggers](managing_loggers.md)
+- [Troubleshooting & limitations](troubleshooting.md)
+
+
 ## API
diff --git a/doc/installation.md b/doc/installation.md
index 991ecb9ce7934253361d596aeadf23bb4254ed3b..6c79ef84a3b70f2ea8f6ba5f92d3c34fc458d1a6 100644
--- a/doc/installation.md
+++ b/doc/installation.md
@@ -8,4 +8,4 @@
 pip install pyotb --upgrade
 ```
 
-For Python>=3.6, latest version available is pyotb 1.4.0. For Python 3.5, latest version available is pyotb 1.2.2
+For Python>=3.6, latest version available is pyotb 1.4.1. For Python 3.5, latest version available is pyotb 1.2.2
diff --git a/doc/interaction.md b/doc/interaction.md
index e8493a8408d14169587fa57feb3b86a37defb556..6c87417d10ecc5d6e7a3556411a331bb099aa3f7 100644
--- a/doc/interaction.md
+++ b/doc/interaction.md
@@ -33,6 +33,7 @@ noisy_image = inp + white_noise  # magic: this is a pyotb object that has the sa
 noisy_image.write('image_plus_noise.tif')
 ```
 Limitations : 
+
 - The whole image is loaded into memory
 - The georeference can not be modified. Thus, numpy operations can not change the image or pixel size
 
@@ -70,9 +71,11 @@ res = scalar_product('image1.tif', 'image2.tif')  # magic: this is a pyotb objec
 ```
 
 Advantages :
+
 - The process supports streaming, hence the whole image is **not** loaded into memory
 - Can be integrated in OTB pipelines
 
 Limitations :
+
 - It is not possible to use the tensorflow python API inside a script where OTBTF is used because of compilation issues 
 between Tensorflow and OTBTF, i.e. `import tensorflow` doesn't work in a script where OTBTF apps have been initialized
diff --git a/doc/troubleshooting.md b/doc/troubleshooting.md
new file mode 100644
index 0000000000000000000000000000000000000000..e6b0b4c375e6c862d31b37d636cb86047ce36700
--- /dev/null
+++ b/doc/troubleshooting.md
@@ -0,0 +1,73 @@
+## Troubleshooting: Known limitations
+
+### Failure of intermediate writing
+
+When chaining applications in-memory, there may be some problems when writing intermediate results, depending on the order
+the writings are requested. Some examples can be found below:
+
+#### Example of failures involving slicing
+
+For some applications (non-exhaustive know list: OpticalCalibration, DynamicConvert, BandMath), we can face unexpected 
+failures when using channels slicing
+```python
+import pyotb
+
+inp = pyotb.DynamicConvert('raster.tif')
+one_band = inp[:, :, 1]
+
+# this works
+one_band.write('one_band.tif')
+
+# this works
+one_band.write('one_band.tif')
+inp.write('stretched.tif')
+
+# this does not work
+inp.write('stretched.tif')
+one_band.write('one_band.tif')  # Failure here
+```
+
+When writing is triggered right after the application declaration, no problem occurs:
+```python
+import pyotb
+
+inp = pyotb.DynamicConvert('raster.tif')
+inp.write('stretched.tif')
+
+one_band = inp[:, :, 1]
+one_band.write('one_band.tif')  # no failure
+```
+
+Also, when using only spatial slicing, no issue has been reported:
+
+```python
+import pyotb
+
+inp = pyotb.DynamicConvert('raster.tif')
+one_band = inp[:100, :100, :]
+
+# this works 
+inp.write('stretched.tif')
+one_band.write('one_band.tif')
+```
+
+
+#### Example of failures involving arithmetic operation
+
+One can meet errors when using arithmetic operations at the end of a pipeline when DynamicConvert, BandMath or
+OpticalCalibration is involved:
+
+```python
+import pyotb
+
+inp = pyotb.DynamicConvert('raster.tif')
+inp_new = pyotb.ManageNoData(inp, mode='changevalue')
+absolute = abs(inp)
+
+# this does not work 
+inp.write('one_band.tif')
+inp_new.write('one_band_nodata.tif')  # Failure here
+absolute.write('absolute.tif')  # Failure here
+```
+
+When writing only the final result, i.e. the end of the pipeline (`absolute.write('absolute.tif')`), there is no problem.
diff --git a/mkdocs.yml b/mkdocs.yml
index 7dcadf54dce2f84959a88133aff7c1c933ffa56c..07c04c26b9dc550aea3f636f5d35ca97c12c99f2 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -27,7 +27,7 @@ plugins:
 nav:
 - Home: index.md
 - Get Started:
-  - Installation: installation.md
+  - Installation of pyotb: installation.md
   - How to use pyotb: quickstart.md
   - Useful features: features.md
   - Functions: functions.md
@@ -39,6 +39,7 @@ nav:
     - Comparison between pyotb and OTB native library: comparison_otb.md
     - OTB versions: otb_versions.md
     - Managing loggers: managing_loggers.md
+    - Troubleshooting & limitations: troubleshooting.md
 - API:
   - pyotb:
       - core: reference/pyotb/core.md
@@ -69,7 +70,7 @@ markdown_extensions:
   - pymdownx.snippets
 
 # rest of the navigation..
-site_name: pyotb
+site_name: "pyotb documentation: a Python extension of OTB"
 repo_url: https://gitlab.orfeo-toolbox.org/nicolasnn/pyotb
 repo_name: pyotb
 docs_dir: doc/
diff --git a/pyotb/__init__.py b/pyotb/__init__.py
index a57a62302ac09232937ed4ad68c00d939a9cc01a..561ef64f648195749ade4e402b353176c49c331f 100644
--- a/pyotb/__init__.py
+++ b/pyotb/__init__.py
@@ -2,7 +2,7 @@
 """
 This module provides convenient python wrapping of otbApplications
 """
-__version__ = "1.4.0"
+__version__ = "1.4.1"
 
 from .apps import *
 from .core import App, Output, Input, get_nbchannels, get_pixel_type
diff --git a/pyotb/core.py b/pyotb/core.py
index 598185adcbf1660024f923250c7d73fa45c421df..791f088469d8177443890f42c6dd993afb244ec2 100644
--- a/pyotb/core.py
+++ b/pyotb/core.py
@@ -120,8 +120,6 @@ class otbObject(ABC):
             if key in dtypes:
                 self.app.SetParameterOutputImagePixelType(key, dtypes[key])
 
-        self.app.PropagateConnectMode(False)
-
         if isinstance(self, App):
             return self.execute()
 
@@ -176,8 +174,9 @@ class otbObject(ABC):
         if isinstance(self, App):
             if not self.finished:
                 self.execute()
-        elif isinstance(self, Output):
-            self.app.Execute()
+        elif isinstance(self, otbObject):
+            if not self.pyotb_app.finished:
+                self.pyotb_app.execute()
 
     # Special methods
     def __getitem__(self, key):
@@ -549,184 +548,12 @@ class otbObject(ABC):
         return NotImplemented
 
 
-class Slicer(otbObject):
-    """Slicer objects i.e. when we call something like raster[:, :, 2] from Python"""
-
-    def __init__(self, x, rows, cols, channels):
-        """
-        Create a slicer object, that can be used directly for writing or inside a BandMath :
-        - an ExtractROI app that handles extracting bands and ROI and can be written to disk or used in pipelines
-        - in case the user only wants to extract one band, an expression such as "im1b#"
-
-        Args:
-            x: input
-            rows: rows slicing (e.g. 100:2000)
-            cols: columns slicing (e.g. 100:2000)
-            channels: channels, can be slicing, list or int
-
-        """
-        # Initialize the app that will be used for writing the slicer
-        self.output_parameter_key = 'out'
-        self.name = 'Slicer'
-        # Trigger source app execution if needed
-        x.execute_if_needed()
-        app = App('ExtractROI', {'in': x, 'mode': 'extent'}, propagate_pixel_type=True)
-        # First exec required in order to read image dim
-        app.app.Execute()
-
-        parameters = {}
-        # Channel slicing
-        nb_channels = get_nbchannels(x)
-        if channels != slice(None, None, None):
-            # if needed, converting int to list
-            if isinstance(channels, int):
-                channels = [channels]
-            # if needed, converting slice to list
-            elif isinstance(channels, slice):
-                channels_start = channels.start if channels.start is not None else 0
-                channels_end = channels.stop if channels.stop is not None else nb_channels
-                channels_step = channels.step if channels.step is not None else 1
-                channels = range(channels_start, channels_end, channels_step)
-            elif isinstance(channels, tuple):
-                channels = list(channels)
-            elif not isinstance(channels, list):
-                raise ValueError(f'Invalid type for channels, should be int, slice or list of bands. : {channels}')
-
-            # Change the potential negative index values to reverse index
-            channels = [c if c >= 0 else nb_channels + c for c in channels]
-            parameters.update({'cl': [f'Channel{i + 1}' for i in channels]})
-
-        # Spatial slicing
-        spatial_slicing = False
-        # TODO: handle PixelValue app so that accessing value is possible, e.g. raster[120, 200, 0]
-        # TODO TBD: handle the step value in the slice so that NN undersampling is possible ? e.g. obj[::2, ::2]
-        if rows.start is not None:
-            parameters.update({'mode.extent.uly': rows.start})
-            spatial_slicing = True
-        if rows.stop is not None and rows.stop != -1:
-            parameters.update(
-                {'mode.extent.lry': rows.stop - 1})  # subtract 1 to be compliant with python convention
-            spatial_slicing = True
-        if cols.start is not None:
-            parameters.update({'mode.extent.ulx': cols.start})
-            spatial_slicing = True
-        if cols.stop is not None and cols.stop != -1:
-            parameters.update(
-                {'mode.extent.lrx': cols.stop - 1})  # subtract 1 to be compliant with python convention
-            spatial_slicing = True
-        # Execute app
-        app.set_parameters(**parameters)
-        app.execute()
-        # Keeping the OTB app, not the pyotb app
-        self.app = app.app
-
-        # These are some attributes when the user simply wants to extract *one* band to be used in an Operation
-        if not spatial_slicing and isinstance(channels, list) and len(channels) == 1:
-            self.one_band_sliced = channels[0] + 1  # OTB convention: channels start at 1
-            self.input = x
-
-
-class Input(otbObject):
-    """
-    Class for transforming a filepath to pyOTB object
-    """
-
-    def __init__(self, filepath):
-        """
-        Args:
-            filepath: raster file path
-
-        """
-        self.output_parameter_key = 'out'
-        self.filepath = filepath
-        self.name = f'Input from {filepath}'
-        app = App('ExtractROI', filepath, execute=True, propagate_pixel_type=True)
-        self.app = app.app
-
-    def __str__(self):
-        """
-        Returns:
-            string representation
-
-        """
-        return f'<pyotb.Input object from {self.filepath}>'
-
-
-class Output(otbObject):
-    """
-    Class for output of an app
-    """
-
-    def __init__(self, app, output_parameter_key):
-        """
-        Args:
-            app: The OTB application
-            output_parameter_key: Output parameter key
-
-        """
-        self.app = app  # keeping a reference of the OTB app
-        self.output_parameter_key = output_parameter_key
-        self.name = f'Output {output_parameter_key} from {self.app.GetName()}'
-
-    def __str__(self):
-        """
-        Returns:
-            string representation
-
-        """
-        return f'<pyotb.Output {self.app.GetName()} object, id {id(self)}>'
-
-
 class App(otbObject):
     """
     Class of an OTB app
     """
     _name = ""
 
-    @property
-    def name(self):
-        """
-        Returns:
-            user's defined name or appname
-
-        """
-        return self._name or self.appname
-
-    @name.setter
-    def name(self, val):
-        """Set custom App name
-
-        Args:
-          val: new name
-
-        """
-        self._name = val
-
-    @property
-    def finished(self):
-        """
-        Property to store whether App has been executed but False if any output file is missing
-
-        Returns:
-            True if exec ended and output files are found else False
-
-        """
-        if self._ended and self.find_output():
-            return True
-        return False
-
-    @finished.setter
-    def finished(self, val):
-        """
-        Value `_ended` will be set to True right after App.execute() or App.write(),
-        then find_output() is called when accessing the property
-
-        Args:
-            val: True if execution ended without exceptions
-
-        """
-        self._ended = val
-
     def __init__(self, appname, *args, execute=False, image_dic=None, otb_stdout=True,
                  pixel_type=None, propagate_pixel_type=False, **kwargs):
         """
@@ -779,9 +606,6 @@ class App(otbObject):
             dtypes = {key: parse_pixel_type(pixel_type) for key in self.output_parameters_keys}
         for key, typ in dtypes.items():
             self.app.SetParameterOutputImagePixelType(key, typ)
-        # Here we make sure that intermediate outputs will be flushed to disk
-        if self.__with_output():
-            self.app.PropagateConnectMode(False)
         # Run app, write output if needed, update `finished` property
         if execute or not self.output_param:
             self.execute()
@@ -789,6 +613,54 @@ class App(otbObject):
         else:
             self.__save_objects()
 
+    def get_output_parameters_keys(self):
+        """Get raster output parameter keys
+
+        Returns:
+            output parameters keys
+        """
+        return [param for param in self.app.GetParametersKeys()
+                if self.app.GetParameterType(param) == otb.ParameterType_OutputImage]
+
+    def set_parameters(self, *args, **kwargs):
+        """Set some parameters of the app. When useful, e.g. for images list, this function appends the parameters
+        instead of overwriting them. Handles any parameters, i.e. in-memory & filepaths
+
+        Args:
+            *args: Can be : - dictionary containing key-arguments enumeration. Useful when a key is python-reserved
+                              (e.g. "in") or contains reserved characters such as a point (e.g."mode.extent.unit")
+                            - string, App or Output, useful when the user implicitly wants to set the param "in"
+                            - list, useful when the user implicitly wants to set the param "il"
+            **kwargs: keyword arguments e.g. il=['input1.tif', oApp_object2, App_object3.out], out='output.tif'
+
+        Raises:
+            Exception: when the setting of a parameter failed
+
+        """
+        parameters = kwargs
+        parameters.update(self.__parse_args(args))
+        # Going through all arguments
+        for param, obj in parameters.items():
+            if param not in self.app.GetParametersKeys():
+                raise Exception(f"{self.name}: parameter '{param}' was not recognized. "
+                                f"Available keys are {self.app.GetParametersKeys()}")
+            # When the parameter expects a list, if needed, change the value to list
+            if self.__is_key_list(param) and not isinstance(obj, (list, tuple)):
+                parameters[param] = [obj]
+                obj = [obj]
+                logger.warning('%s: Argument for parameter "%s" was converted to list', self.name, param)
+            try:
+                # This is when we actually call self.app.SetParameter*
+                self.__set_param(param, obj)
+            except (RuntimeError, TypeError, ValueError, KeyError) as e:
+                raise Exception(f"{self.name}: something went wrong before execution "
+                                f"(while setting parameter {param} to '{obj}')") from e
+
+        # Update App's parameters attribute
+        self.parameters.update(parameters)
+        if self.preserve_dtype:
+            self.__propagate_pixel_type()
+
     def execute(self):
         """
         Execute with appropriate and outputs to disk if any output parameter was set
@@ -797,12 +669,11 @@ class App(otbObject):
              boolean flag that indicate if command executed with success
 
         """
-        success = False
         logger.debug("%s: run execute() with parameters=%s", self.name, self.parameters)
         try:
             self.app.Execute()
             if self.__with_output():
-                self.app.WriteOutput()
+                self.app.ExecuteAndWriteOutput()
             self.finished = True
         except (RuntimeError, FileNotFoundError) as e:
             raise Exception(f'{self.name}: error during during app execution') from e
@@ -813,6 +684,50 @@ class App(otbObject):
 
         return success
 
+    @property
+    def name(self):
+        """
+        Returns:
+            user's defined name or appname
+
+        """
+        return self._name or self.appname
+
+    @name.setter
+    def name(self, val):
+        """Set custom App name
+
+        Args:
+          val: new name
+
+        """
+        self._name = val
+
+    @property
+    def finished(self):
+        """
+        Property to store whether App has been executed but False if any output file is missing
+
+        Returns:
+            True if exec ended and output files are found else False
+
+        """
+        if self._ended and self.find_output():
+            return True
+        return False
+
+    @finished.setter
+    def finished(self, val):
+        """
+        Value `_ended` will be set to True right after App.execute() or App.write(),
+        then find_output() is called when accessing the property
+
+        Args:
+            val: True if execution ended without exceptions
+
+        """
+        self._ended = val
+
     def find_output(self):
         """
         Find output files on disk using parameters
@@ -864,54 +779,6 @@ class App(otbObject):
         if memory:
             self.app.FreeRessources()
 
-    def get_output_parameters_keys(self):
-        """Get raster output parameter keys
-
-        Returns:
-            output parameters keys
-        """
-        return [param for param in self.app.GetParametersKeys()
-                if self.app.GetParameterType(param) == otb.ParameterType_OutputImage]
-
-    def set_parameters(self, *args, **kwargs):
-        """Set some parameters of the app. When useful, e.g. for images list, this function appends the parameters
-        instead of overwriting them. Handles any parameters, i.e. in-memory & filepaths
-
-        Args:
-            *args: Can be : - dictionary containing key-arguments enumeration. Useful when a key is python-reserved
-                              (e.g. "in") or contains reserved characters such as a point (e.g."mode.extent.unit")
-                            - string, App or Output, useful when the user implicitly wants to set the param "in"
-                            - list, useful when the user implicitly wants to set the param "il"
-            **kwargs: keyword arguments e.g. il=['input1.tif', oApp_object2, App_object3.out], out='output.tif'
-
-        Raises:
-            Exception: when the setting of a parameter failed
-
-        """
-        parameters = kwargs
-        parameters.update(self.__parse_args(args))
-        # Going through all arguments
-        for param, obj in parameters.items():
-            if param not in self.app.GetParametersKeys():
-                raise Exception(f"{self.name}: parameter '{param}' was not recognized. "
-                                f"Available keys are {self.app.GetParametersKeys()}")
-            # When the parameter expects a list, if needed, change the value to list
-            if self.__is_key_list(param) and not isinstance(obj, (list, tuple)):
-                parameters[param] = [obj]
-                obj = [obj]
-                logger.warning('%s: Argument for parameter "%s" was converted to list', self.name, param)
-            try:
-                # This is when we actually call self.app.SetParameter*
-                self.__set_param(param, obj)
-            except (RuntimeError, TypeError, ValueError, KeyError) as e:
-                raise Exception(f"{self.name}: something went wrong before execution "
-                                f"(while setting parameter {param} to '{obj}')") from e
-
-        # Update App's parameters attribute
-        self.parameters.update(parameters)
-        if self.preserve_dtype:
-            self.__propagate_pixel_type()
-
     # Private functions
     @staticmethod
     def __parse_args(args):
@@ -932,33 +799,34 @@ class App(otbObject):
         """
         Set one parameter, decide which otb.Application method to use depending on target object
         """
-        # Single-parameter cases
-        if isinstance(obj, otbObject):
-            self.app.ConnectImage(param, obj.app, obj.output_param)
-        elif isinstance(obj, otb.Application):  # this is for backward comp with plain OTB
-            outparamkey = [param for param in obj.GetParametersKeys()
-                           if obj.GetParameterType(param) == otb.ParameterType_OutputImage][0]
-            self.app.ConnectImage(param, obj, outparamkey)
-        elif param == 'ram':  # SetParameterValue in OTB<7.4 doesn't work for ram parameter cf gitlab OTB issue 2200
-            self.app.SetParameterInt('ram', int(obj))
-        elif not isinstance(obj, list):  # any other parameters (str, int...)
-            self.app.SetParameterValue(param, obj)
-        # Images list
-        elif self.__is_key_images_list(param):
-            # To enable possible in-memory connections, we go through the list and set the parameters one by one
-            for inp in obj:
-                if isinstance(inp, otbObject):
-                    self.app.ConnectImage(param, inp.app, inp.output_param)
-                elif isinstance(inp, otb.Application):  # this is for backward comp with plain OTB
-                    outparamkey = [param for param in inp.GetParametersKeys() if
-                                   inp.GetParameterType(param) == otb.ParameterType_OutputImage][0]
-                    self.app.ConnectImage(param, inp, outparamkey)
-                else:  # here `input` should be an image filepath
-                    # Append `input` to the list, do not overwrite any previously set element of the image list
-                    self.app.AddParameterStringList(param, inp)
-        # List of any other types (str, int...)
-        else:
-            self.app.SetParameterValue(param, obj)
+        if obj is not None:
+            # Single-parameter cases
+            if isinstance(obj, otbObject):
+                self.app.ConnectImage(param, obj.app, obj.output_param)
+            elif isinstance(obj, otb.Application):  # this is for backward comp with plain OTB
+                outparamkey = [param for param in obj.GetParametersKeys()
+                               if obj.GetParameterType(param) == otb.ParameterType_OutputImage][0]
+                self.app.ConnectImage(param, obj, outparamkey)
+            elif param == 'ram':  # SetParameterValue in OTB<7.4 doesn't work for ram parameter cf gitlab OTB issue 2200
+                self.app.SetParameterInt('ram', int(obj))
+            elif not isinstance(obj, list):  # any other parameters (str, int...)
+                self.app.SetParameterValue(param, obj)
+            # Images list
+            elif self.__is_key_images_list(param):
+                # To enable possible in-memory connections, we go through the list and set the parameters one by one
+                for inp in obj:
+                    if isinstance(inp, otbObject):
+                        self.app.ConnectImage(param, inp.app, inp.output_param)
+                    elif isinstance(inp, otb.Application):  # this is for backward comp with plain OTB
+                        outparamkey = [param for param in inp.GetParametersKeys() if
+                                       inp.GetParameterType(param) == otb.ParameterType_OutputImage][0]
+                        self.app.ConnectImage(param, inp, outparamkey)
+                    else:  # here `input` should be an image filepath
+                        # Append `input` to the list, do not overwrite any previously set element of the image list
+                        self.app.AddParameterStringList(param, inp)
+            # List of any other types (str, int...)
+            else:
+                self.app.SetParameterValue(param, obj)
 
     def __propagate_pixel_type(self):
         """
@@ -992,7 +860,7 @@ class App(otbObject):
         """
         for key in self.app.GetParametersKeys():
             if key in self.output_parameters_keys:  # raster outputs
-                output = Output(self.app, key)
+                output = Output(self, key)
                 setattr(self, key, output)
             elif key not in ('parameters',):  # any other attributes (scalars...)
                 try:
@@ -1004,22 +872,15 @@ class App(otbObject):
         """
         Check if a key of the App is an input parameter list
         """
-        return self.app.GetParameterType(key) in (
-            otb.ParameterType_InputImageList,
-            otb.ParameterType_StringList,
-            otb.ParameterType_InputFilenameList,
-            otb.ParameterType_InputVectorDataList,
-            otb.ParameterType_ListView
-        )
+        return self.app.GetParameterType(key) in (otb.ParameterType_InputImageList, otb.ParameterType_StringList,
+                                                  otb.ParameterType_InputFilenameList, otb.ParameterType_ListView,
+                                                  otb.ParameterType_InputVectorDataList)
 
     def __is_key_images_list(self, key):
         """
         Check if a key of the App is an input parameter image list
         """
-        return self.app.GetParameterType(key) in (
-            otb.ParameterType_InputImageList,
-            otb.ParameterType_InputFilenameList
-        )
+        return self.app.GetParameterType(key) in (otb.ParameterType_InputImageList, otb.ParameterType_InputFilenameList)
 
     # Special methods
     def __str__(self):
@@ -1029,6 +890,135 @@ class App(otbObject):
         return f'<pyotb.App {self.appname} object id {id(self)}>'
 
 
+class Slicer(otbObject):
+    """Slicer objects i.e. when we call something like raster[:, :, 2] from Python"""
+
+    def __init__(self, x, rows, cols, channels):
+        """
+        Create a slicer object, that can be used directly for writing or inside a BandMath. It contains :
+        - an ExtractROI app that handles extracting bands and ROI and can be written to disk or used in pipelines
+        - in case the user only wants to extract one band, an expression such as "im1b#"
+
+        Args:
+            x: input
+            rows: rows slicing (e.g. 100:2000)
+            cols: columns slicing (e.g. 100:2000)
+            channels: channels, can be slicing, list or int
+
+        """
+        # Initialize the app that will be used for writing the slicer
+        self.output_parameter_key = 'out'
+        self.name = 'Slicer'
+        app = App('ExtractROI', {'in': x, 'mode': 'extent'}, propagate_pixel_type=True)
+
+        parameters = {}
+        # Channel slicing
+        if channels != slice(None, None, None):
+            # Trigger source app execution if needed
+            nb_channels = get_nbchannels(x)
+            app.app.Execute()  # this is needed by ExtractROI for setting the `cl` parameter
+            # if needed, converting int to list
+            if isinstance(channels, int):
+                channels = [channels]
+            # if needed, converting slice to list
+            elif isinstance(channels, slice):
+                channels_start = channels.start if channels.start is not None else 0
+                channels_end = channels.stop if channels.stop is not None else nb_channels
+                channels_step = channels.step if channels.step is not None else 1
+                channels = range(channels_start, channels_end, channels_step)
+            elif isinstance(channels, tuple):
+                channels = list(channels)
+            elif not isinstance(channels, list):
+                raise ValueError(f'Invalid type for channels, should be int, slice or list of bands. : {channels}')
+
+            # Change the potential negative index values to reverse index
+            channels = [c if c >= 0 else nb_channels + c for c in channels]
+            parameters.update({'cl': [f'Channel{i + 1}' for i in channels]})
+
+        # Spatial slicing
+        spatial_slicing = False
+        # TODO: handle PixelValue app so that accessing value is possible, e.g. raster[120, 200, 0]
+        # TODO TBD: handle the step value in the slice so that NN undersampling is possible ? e.g. raster[::2, ::2]
+        if rows.start is not None:
+            parameters.update({'mode.extent.uly': rows.start})
+            spatial_slicing = True
+        if rows.stop is not None and rows.stop != -1:
+            parameters.update(
+                {'mode.extent.lry': rows.stop - 1})  # subtract 1 to be compliant with python convention
+            spatial_slicing = True
+        if cols.start is not None:
+            parameters.update({'mode.extent.ulx': cols.start})
+            spatial_slicing = True
+        if cols.stop is not None and cols.stop != -1:
+            parameters.update(
+                {'mode.extent.lrx': cols.stop - 1})  # subtract 1 to be compliant with python convention
+            spatial_slicing = True
+        # Execute app
+        app.set_parameters(**parameters)
+
+        # Keeping the OTB app and the pyotb app
+        self.pyotb_app, self.app = app, app.app
+
+        # These are some attributes when the user simply wants to extract *one* band to be used in an Operation
+        if not spatial_slicing and isinstance(channels, list) and len(channels) == 1:
+            self.one_band_sliced = channels[0] + 1  # OTB convention: channels start at 1
+            self.input = x
+
+
+class Input(otbObject):
+    """
+    Class for transforming a filepath to pyOTB object
+    """
+
+    def __init__(self, filepath):
+        """
+        Args:
+            filepath: raster file path
+
+        """
+        self.output_parameter_key = 'out'
+        self.filepath = filepath
+        self.name = f'Input from {filepath}'
+        app = App('ExtractROI', filepath, execute=True, propagate_pixel_type=True)
+
+        # Keeping the OTB app and the pyotb app
+        self.pyotb_app, self.app = app, app.app
+
+    def __str__(self):
+        """
+        Returns:
+            string representation
+
+        """
+        return f'<pyotb.Input object from {self.filepath}>'
+
+
+class Output(otbObject):
+    """
+    Class for output of an app
+    """
+
+    def __init__(self, app, output_parameter_key):
+        """
+        Args:
+            app: The pyotb App
+            output_parameter_key: Output parameter key
+
+        """
+        # Keeping the OTB app and the pyotb app
+        self.pyotb_app, self.app = app, app.app
+        self.output_parameter_key = output_parameter_key
+        self.name = f'Output {output_parameter_key} from {self.app.GetName()}'
+
+    def __str__(self):
+        """
+        Returns:
+            string representation
+
+        """
+        return f'<pyotb.Output {self.app.GetName()} object, id {id(self)}>'
+
+
 class Operation(otbObject):
     """
     Class for arithmetic/math operations done in Python.
@@ -1055,7 +1045,7 @@ class Operation(otbObject):
         """
         Given some inputs and an operator, this function enables to transform this into an OTB application.
         Operations generally involve 2 inputs (+, -...). It can have only 1 input for `abs` operator.
-        It can have 3 inputs for the ternary operator `cond ? x : y`,
+        It can have 3 inputs for the ternary operator `cond ? x : y`.
 
         Args:
             operator: (str) one of +, -, *, /, >, <, >=, <=, ==, !=, &, |, abs, ?
@@ -1101,8 +1091,9 @@ class Operation(otbObject):
             app = App('BandMath', il=self.unique_inputs, exp=self.exp)
         else:
             app = App('BandMathX', il=self.unique_inputs, exp=self.exp)
-        app.execute()
-        self.app = app.app
+
+        # Keeping the OTB app and the pyotb app
+        self.pyotb_app, self.app = app, app.app
 
     def create_fake_exp(self, operator, inputs, nb_bands=None):
         """
diff --git a/setup.py b/setup.py
index 7785ddcd494292cd0b9d5f2896304162759d5656..c99a42266e03f592d0fe63eead5ace726d57b207 100644
--- a/setup.py
+++ b/setup.py
@@ -6,7 +6,7 @@ with open("README.md", "r", encoding="utf-8") as fh:
 
 setuptools.setup(
     name="pyotb",
-    version="1.4.0",
+    version="1.4.1",
     author="Nicolas Narçon",
     author_email="nicolas.narcon@gmail.com",
     description="Library to enable easy use of the Orfeo Tool Box (OTB) in Python",
diff --git a/tests/pipeline_test.py b/tests/pipeline_test.py
index 3b152b5a01a08115aa4746c8a42a750eaf65687f..a3d7ff420ec7aa581708571e58d6d404ad96f0e8 100644
--- a/tests/pipeline_test.py
+++ b/tests/pipeline_test.py
@@ -21,6 +21,10 @@ PYOTB_BLOCKS = [
 
 ALL_BLOCKS = PYOTB_BLOCKS + OTBAPPS_BLOCKS
 
+# These apps are problematic when used in pipelines with intermediate outputs
+# (cf https://gitlab.orfeo-toolbox.org/orfeotoolbox/otb/-/issues/2290)
+PROBLEMATIC_APPS = ['DynamicConvert', 'BandMath']
+
 
 def backward():
     """
@@ -52,7 +56,7 @@ def check_app_write(app, out):
 
 filepath = 'Data/Input/QB_MUL_ROI_1000_100.tif'
 pyotb_input = pyotb.Input(filepath)
-
+args = [arg.lower() for arg in sys.argv[1:]] if len(sys.argv) > 1 else []
 
 def generate_pipeline(inp, building_blocks):
     """
@@ -104,25 +108,27 @@ def test_pipeline(pipeline):
     report = {"shapes_errs": [], "write_errs": []}
 
     # Test outputs shapes
-    generator = enumerate(pipeline)
-    if len(sys.argv) > 1:
-        if "backward" in sys.argv[1].lower():
-            print("Perform tests in backward mode")
-            generator = enumerate(reversed(pipeline))
-    for i, app in generator:
-        try:
-            print(f"Trying to access shape of app {app.name} output...")
-            shape = app.shape
-            print(f"App {app.name} output shape is {shape}")
-        except Exception as e:
-            print("\n\033[91mGET SHAPE ERROR\033[0m")
-            print(e)
-            report["shapes_errs"].append(i)
+    pipeline_items = [pipeline[-1]] if "no-intermediate-output" in args else pipeline
+    generator = lambda: enumerate(pipeline_items)
+    if "backward" in args:
+        print("Perform tests in backward mode")
+        generator = lambda: enumerate(reversed(pipeline_items))
+    if "shape" in args:
+        for i, app in generator():
+            try:
+                print(f"Trying to access shape of app {app.name} output...")
+                shape = app.shape
+                print(f"App {app.name} output shape is {shape}")
+            except Exception as e:
+                print("\n\033[91mGET SHAPE ERROR\033[0m")
+                print(e)
+                report["shapes_errs"].append(i)
 
     # Test all pipeline outputs
-    for i, app in generator:
-        if not check_app_write(app, f"/tmp/out_{i}.tif"):
-            report["write_errs"].append(i)
+    if "write" in args:
+        for i, app in generator():
+            if not check_app_write(app, f"/tmp/out_{i}.tif"):
+                report["write_errs"].append(i)
 
     return report
 
@@ -161,10 +167,11 @@ for pipeline in pipelines:
 
 # Summary
 cols = max([len(pipeline2str(pipeline)) for pipeline in pipelines]) + 1
-print("Tests summary:")
+print(f'Tests summary (\033[93mTest options: {"; ".join(args)}\033[0m)')
 print("Pipeline".ljust(cols) + " | Status (reason)")
 print("-" * cols + "-|-" + "-" * 20)
 nb_fails = 0
+allowed_to_fail = 0
 for pipeline, errs in results.items():
     has_err = sum(len(value) for key, value in errs.items()) > 0
     graph = pipeline2str(pipeline)
@@ -173,12 +180,19 @@ for pipeline, errs in results.items():
         msg = f"\033[91m{msg}\033[0m"
     msg += " | "
     if has_err:
-        nb_fails += 1
-        causes = [f"{section}: " + ", ".join([str(i) for i in out_ids])
+        causes = [f"{section}: " + ", ".join([f"app{i}" for i in out_ids])
                   for section, out_ids in errs.items() if out_ids]
         msg += "\033[91mFAIL\033[0m (" + "; ".join(causes) + ")"
+
+        # There is a failure when the pipeline length is >=3, the last app is an Operation and the first app of the
+        # piepline is one of the problematic apps
+        if ("write" in args and "backward" not in args and isinstance(pipeline[-1], pyotb.Operation)
+            and len(pipeline) == 3 and pipeline[0].name in PROBLEMATIC_APPS):
+            allowed_to_fail += 1
+        else:
+            nb_fails += 1
     else:
         msg += "\033[92mPASS\033[0m"
     print(msg)
-print(f"End of summary ({nb_fails} error(s)).", flush=True)
+print(f"End of summary ({nb_fails} error(s), {allowed_to_fail} 'allowed to fail' error(s))", flush=True)
 assert nb_fails == 0, "One of the pipelines have failed. Please read the report."