Repository: Ikomia-dev/IkomiaApi (language: python)

Issue:
Get more complete callstack when algorithms failed to load
**Describe the bug**
When algorithm failed to load due to code errors or missing dependencies, exception is raised as attended but callstack does not contain complete information about the root cause.

**Expected behavior**
Get a complete callstack to understand the root cause of the failed loading.

**Environment:**
 - OS:  Windows11, Linux
 - Python version: 3.7 - 3.10
 - Ikomia API version: 0.10.0



Discussion / hints:

Fix patch:
diff --git a/doc/source/_autosummary/_autosummary/ikomia.dataprocess.pydataprocess.CWorkflow.rst b/doc/source/_autosummary/_autosummary/ikomia.dataprocess.pydataprocess.CWorkflow.rst
index aa4dfc01..ac5d1332 100644
--- a/doc/source/_autosummary/_autosummary/ikomia.dataprocess.pydataprocess.CWorkflow.rst
+++ b/doc/source/_autosummary/_autosummary/ikomia.dataprocess.pydataprocess.CWorkflow.rst
@@ -15,11 +15,14 @@ CWorkflow
     .. autosummary::
 
         ~CWorkflow.__init__
+        ~CWorkflow.add_exposed_parameter
         ~CWorkflow.add_input
+        ~CWorkflow.add_output
         ~CWorkflow.add_task
         ~CWorkflow.clear
         ~CWorkflow.clear_inputs
         ~CWorkflow.clear_output_data
+        ~CWorkflow.clear_outputs
         ~CWorkflow.connect
         ~CWorkflow.delete_edge
         ~CWorkflow.delete_task
@@ -29,10 +32,15 @@ CWorkflow
         ~CWorkflow.get_edge_source
         ~CWorkflow.get_edge_target
         ~CWorkflow.get_elapsed_time_to
+        ~CWorkflow.get_exposed_parameters
         ~CWorkflow.get_final_tasks
         ~CWorkflow.get_in_edges
         ~CWorkflow.get_last_run_folder
         ~CWorkflow.get_out_edges
+        ~CWorkflow.get_output_count
+        ~CWorkflow.get_outputs
+        ~CWorkflow.get_output
+        ~CWorkflow.get_output_data_type
         ~CWorkflow.get_parents
         ~CWorkflow.get_root_id
         ~CWorkflow.get_root_target_types
@@ -45,6 +53,7 @@ CWorkflow
         ~CWorkflow.run
         ~CWorkflow.save
         ~CWorkflow.set_auto_save
+        ~CWorkflow.set_exposed_parameter
         ~CWorkflow.set_input
         ~CWorkflow.set_output_folder
         ~CWorkflow.stop
@@ -60,11 +69,15 @@ CWorkflow
     .. rubric:: Details
 
     .. automethod:: __init__
+    .. automethod:: add_exposed_parameter
     .. automethod:: add_input
+    .. automethod:: add_output
     .. automethod:: add_task
     .. automethod:: clear
     .. automethod:: clear_inputs
     .. automethod:: clear_output_data
+    .. automethod:: clear_outputs
+    .. automethod:: clear_parameters
     .. automethod:: connect
     .. automethod:: delete_edge
     .. automethod:: delete_task
@@ -74,10 +87,15 @@ CWorkflow
     .. automethod:: get_edge_source
     .. automethod:: get_edge_target
     .. automethod:: get_elapsed_time_to
+    .. automethod:: get_exposed_parameters
     .. automethod:: get_final_tasks
     .. automethod:: get_in_edges
     .. automethod:: get_last_run_folder
     .. automethod:: get_out_edges
+    .. automethod:: get_output_count
+    .. automethod:: get_outputs
+    .. automethod:: get_output
+    .. automethod:: get_output_data_type
     .. automethod:: get_parents
     .. automethod:: get_root_id
     .. automethod:: get_root_target_types
@@ -90,6 +108,7 @@ CWorkflow
     .. automethod:: run
     .. automethod:: save
     .. automethod:: set_auto_save
+    .. automethod:: set_exposed_parameters
     .. automethod:: set_input
     .. automethod:: set_output_folder
     .. automethod:: stop
diff --git a/doc/source/_autosummary/_autosummary/ikomia.dataprocess.workflow.Workflow.rst b/doc/source/_autosummary/_autosummary/ikomia.dataprocess.workflow.Workflow.rst
index ea3bd8b1..64e74e03 100644
--- a/doc/source/_autosummary/_autosummary/ikomia.dataprocess.workflow.Workflow.rst
+++ b/doc/source/_autosummary/_autosummary/ikomia.dataprocess.workflow.Workflow.rst
@@ -22,15 +22,18 @@ Workflow
         ~Workflow.get_task_output
         ~Workflow.get_tasks
         ~Workflow.get_time_metrics
+        ~Workflow.get_workflow_parameters
         ~Workflow.load
         ~Workflow.remove_task
         ~Workflow.root
         ~Workflow.run
         ~Workflow.run_on
+        ~Workflow.save
         ~Workflow.set_directory_input
         ~Workflow.set_image_input
         ~Workflow.set_video_input
         ~Workflow.set_parameters
+        ~Workflow.set_workflow_parameters
 
 
     .. rubric:: Inherited methods
@@ -59,12 +62,10 @@ Workflow
         ~Workflow.get_task_count
         ~Workflow.get_task_ids
         ~Workflow.get_total_elapsed_time
-        ~Workflow.load
         ~Workflow.remove_input
         ~Workflow.set_auto_save
         ~Workflow.set_input
         ~Workflow.set_output_folder
-        ~Workflow.save
         ~Workflow.stop
         ~Workflow.update_start_time
 
@@ -102,6 +103,7 @@ Workflow
     .. automethod:: get_tasks
     .. automethod:: get_time_metrics
     .. automethod:: get_total_elapsed_time
+    .. automethod:: get_workflow_parameters
     .. automethod:: load
     .. automethod:: remove_input
     .. automethod:: remove_task
@@ -115,6 +117,7 @@ Workflow
     .. automethod:: set_input
     .. automethod:: set_output_folder
     .. automethod:: set_parameters
+    .. automethod:: set_workflow_parameters
     .. automethod:: save
     .. automethod:: stop
     .. automethod:: update_start_time
diff --git a/ikomia/core/__init__.py b/ikomia/core/__init__.py
index 6d63ca9b..a0fa4e3f 100644
--- a/ikomia/core/__init__.py
+++ b/ikomia/core/__init__.py
@@ -37,7 +37,7 @@ def _check_directories():
     os.makedirs(config.main_cfg["tensorboard"]["log_uri"], exist_ok=True)
 
 
-def get_ikomia_root_folder():
+def get_ikomia_root_folder() -> str:
     return config.main_cfg["root_folder"]
 
 
diff --git a/ikomia/core/auth.py b/ikomia/core/auth.py
index 08dbba43..7d3d2c5f 100644
--- a/ikomia/core/auth.py
+++ b/ikomia/core/auth.py
@@ -56,7 +56,7 @@ def authenticate(self, token: str = None, username: str = None, password: str =
         r = self.session.get(url)
         r.raise_for_status()
 
-    def is_authenticated(self):
+    def is_authenticated(self) -> bool:
         if self.token is None:
             return False
 
diff --git a/ikomia/core/config.py b/ikomia/core/config.py
index 5b455da0..b2a130f1 100644
--- a/ikomia/core/config.py
+++ b/ikomia/core/config.py
@@ -15,8 +15,6 @@
 import os
 import yaml
 
-global main_cfg
-
 directory = os.path.dirname(os.path.abspath(__file__))
 main_cfg = yaml.safe_load(open(directory + "/config.yaml"))
 
diff --git a/ikomia/core/task.py b/ikomia/core/task.py
index 7847afbf..a6e6e5b7 100644
--- a/ikomia/core/task.py
+++ b/ikomia/core/task.py
@@ -19,12 +19,13 @@
 """
 import logging
 import ikomia
-from ikomia import core, dataprocess
+from ikomia.core import CWorkflowTaskParam, CWorkflowTask
+from ikomia.dataprocess import CWorkflowTaskIO
 
 logger = logging.getLogger(__name__)
 
 
-class TaskParam(core.CWorkflowTaskParam):
+class TaskParam(CWorkflowTaskParam):
     """
     Base class to manage task parameters. Inherit :py:class:`~ikomia.core.pycore.CWorkflowTaskParam`.
     It includes a dict structure to store parameter values.
@@ -35,10 +36,10 @@ def __init__(self):
         """
         Constructor. Initialize an empty dict structure.
         """
-        core.CWorkflowTaskParam.__init__(self)
+        CWorkflowTaskParam.__init__(self)
         self.cfg = {}
 
-    def get_values(self):
+    def get_values(self) -> dict:
         """
         Return parameters for saving in Ikomia Studio.
         """
@@ -48,14 +49,14 @@ def get_values(self):
 
         return param_map
 
-    def set_values(self, params):
+    def set_values(self, params: dict):
         """
         Generic way to set parameters of CWorkflowTask-based object. Must be reimplemented.
         """
         raise NotImplementedError
 
 
-def create(name="", public_hub:bool=True, private_hub:bool=False):
+def create(name: str = "", public_hub: bool = True, private_hub: bool = False) -> CWorkflowTask:
     """
     Create task instance (ie algorithm) from the given name.
     See :py:class:`~ikomia.dataprocess.registry.IkomiaRegistry` for details.
@@ -74,7 +75,7 @@ def create(name="", public_hub:bool=True, private_hub:bool=False):
     return ikomia.ik_registry.create_algorithm(name=name, public_hub=public_hub, private_hub=private_hub)
 
 
-def get_output(task_obj, types:list, index:int=-1):
+def get_output(task_obj: CWorkflowTask, types: list, index: int = -1) -> CWorkflowTaskIO:
     """
     Get specific output(s) of a task from the given types (:py:class:`~ikomia.core.pycore.IODataType`).
 
@@ -102,3 +103,24 @@ def get_output(task_obj, types:list, index:int=-1):
         return outputs[index]
     else:
         raise RuntimeError(f"No output at index {index}: only {len(outputs)} outputs available")
+
+
+def conform_parameters(params: dict) -> dict:
+    """
+    Conform parameters dict structure to ensure that all values are string.
+
+    Args:
+        params (dict): parameters as key-value pairs
+    """
+    valid_params = {}
+    for key in params:
+        if type(key) is not str:
+            raise TypeError("Parameter key must be a string.")
+
+        value = params[key]
+        if type(params[key]) is not str:
+            valid_params[key] = str(value)
+        else:
+            valid_params[key] = value
+
+    return valid_params
diff --git a/ikomia/dataprocess/datadictIO.py b/ikomia/dataprocess/datadictIO.py
index a49fd9f9..81759e65 100644
--- a/ikomia/dataprocess/datadictIO.py
+++ b/ikomia/dataprocess/datadictIO.py
@@ -39,7 +39,7 @@ def clear_data(self):
         """
         self.data.clear()
 
-    def is_data_available(self):
+    def is_data_available(self) -> bool:
         """
         Check whether the dataset structure contains data.
 
@@ -48,27 +48,27 @@ def is_data_available(self):
         """
         return len(self.data) > 0
 
-    def save(self, path):
+    def save(self, path: str):
         """
         Save data dict as JSON.
 
         Args:
-            path: file path where data is saved
+            path (str): file path where data is saved
         """
         with open(path, "w") as outfile:
             json.dump(self.data, outfile)
 
-    def load(self, path):
+    def load(self, path: str):
         """
         Load JSON as data dict I/O.
 
         Args:
-            path: file path where dataset is saved
+            path (str): file path where dataset is saved
         """
         with open(path, "r") as infile:
             self.data = json.load(infile)
 
-    def to_json(self, options=[]):
+    def to_json(self, options: list = []) -> str:
         json_format = "compact"
         if "json_format" in options:
             json_format = options[options.index("json_format") + 1]
@@ -78,5 +78,5 @@ def to_json(self, options=[]):
         else:
             return json.dumps(self.data)
 
-    def from_json(self, json_string):
+    def from_json(self, json_string: str):
         self.data = json.loads(json_string)
diff --git a/ikomia/dataprocess/registry.py b/ikomia/dataprocess/registry.py
index f5220135..cb1687bc 100644
--- a/ikomia/dataprocess/registry.py
+++ b/ikomia/dataprocess/registry.py
@@ -18,9 +18,9 @@
 algorithms from the built-in environment or Ikomia HUB.
 """
 import ikomia
-from ikomia import utils, dataprocess
-from ikomia.utils import autocomplete
-from ikomia.core import config
+from ikomia import utils
+from ikomia.core import config, CWorkflowTaskParam, CWorkflowTask
+from ikomia.dataprocess import CIkomiaRegistry
 import os
 import sys
 import logging
@@ -31,18 +31,19 @@
 import re
 import time
 from datetime import datetime
+from typing import Union
 
 logger = logging.getLogger(__name__)
 
 
-class IkomiaRegistry(dataprocess.CIkomiaRegistry):
+class IkomiaRegistry(CIkomiaRegistry):
     """
     Registry for all Ikomia algorithms (built-in and Ikomia HUB). It stores all algorithms references and allows to
     install, update and instanciate any of these algorithms.
     Derived from :py:class:`~ikomia.dataprocess.pydataprocess.CIkomiaRegistry`.
     """
-    def __init__(self, lazy_load:bool=True):
-        dataprocess.CIkomiaRegistry.__init__(self)
+    def __init__(self, lazy_load: bool = True):
+        CIkomiaRegistry.__init__(self)
         self.public_online_algos = None
         self.private_online_algos = None
 
@@ -52,7 +53,7 @@ def __init__(self, lazy_load:bool=True):
     def __repr__(self):
         return f"IkomiaRegistry()"
 
-    def get_public_hub_algorithms(self, force:bool=False):
+    def get_public_hub_algorithms(self, force: bool = False) -> list:
         """
         Get the list of available algorithms from public Ikomia HUB.
         Each algorithm is identified by a unique name.
@@ -74,7 +75,7 @@ def get_public_hub_algorithms(self, force:bool=False):
 
         return self.public_online_algos
 
-    def get_private_hub_algorithms(self, force:bool=False):
+    def get_private_hub_algorithms(self, force:bool=False) -> list:
         """
         Get the list of available algorithms from private Ikomia HUB (authentication required).
         Each algorithm is identified by a unique name.
@@ -97,7 +98,7 @@ def get_private_hub_algorithms(self, force:bool=False):
 
         return self.private_online_algos
 
-    def _get_online_algos(self, url: str):
+    def _get_online_algos(self, url: str) -> list:
         s = ikomia.ik_api_session
         algos = self._get_all_from_pagination(url)
         valid_algos = []
@@ -113,7 +114,7 @@ def _get_online_algos(self, url: str):
 
         return valid_algos
 
-    def _get_all_from_pagination(self, url: str):
+    def _get_all_from_pagination(self, url: str) -> dict:
         s = ikomia.ik_api_session
         r = s.session.get(url)
         r.raise_for_status()
@@ -127,7 +128,8 @@ def _get_all_from_pagination(self, url: str):
 
         return pagination_data["results"]
 
-    def create_algorithm(self, name:str, parameters=None, public_hub:bool=True, private_hub=False):
+    def create_algorithm(self, name:str, parameters: CWorkflowTaskParam = None,
+                         public_hub: bool = True, private_hub: bool = False) -> CWorkflowTask:
         """
         Instanciate algorithm from its unique name. See :py:meth:`~ikomia.dataprocess.IkomiaRegistry.get_algorithms` or
         :py:meth:`~ikomia.dataprocess.IkomiaRegistry.get_public_hub_algorithms` or
@@ -171,8 +173,7 @@ def create_algorithm(self, name:str, parameters=None, public_hub:bool=True, priv
                 else:
                     # If algorithm is installed locally but not functional (algo_dir is not empty), it may be a plugin
                     # in developpement and we should not overwrite it with the Ikomia Hub version
-                    raise RuntimeError(f"Algorithm {name} is installed locally but not functional. "
-                                       f"Check your code or your Python environment please.")
+                    raise RuntimeError(f"Algorithm {name} is installed locally but not functional: {e}")
 
         return algo
 
@@ -240,7 +241,7 @@ def update_algorithm(self, name:str, public_hub:bool=True, private_hub:bool=Fals
 
         raise RuntimeError(f"Algorithm {name} does not exist in Ikomia HUB")
 
-    def _has_to_be_updated(self, name:str, hub_info: dict):
+    def _has_to_be_updated(self, name:str, hub_info: dict) -> bool:
         local_info = self.get_algorithm_info(name)
         current_version = semver.Version.parse(local_info.version)
 
@@ -257,7 +258,7 @@ def _has_to_be_updated(self, name:str, hub_info: dict):
             local_modif_date = datetime.strptime(local_format_time, "%Y-%m-%dT%H:%M:%S")
             return hub_modif_date > local_modif_date
 
-    def _find_hub_algo(self, name: str, hub_list: list):
+    def _find_hub_algo(self, name: str, hub_list: list) -> Union[dict, None]:
         for algo in hub_list:
             if algo["name"] == name:
                 return algo
@@ -299,9 +300,9 @@ def install_algorithm(self, name:str, public_hub: bool = True, private_hub: bool
                            f"It will be updated on next start.")
 
         if config.main_cfg["registry"]["auto_completion"]:
-            autocomplete.update_local_plugin(name)
+            utils.autocomplete.update_local_plugin(name)
 
-    def _get_algorithm_directory(self, name:str):
+    def _get_algorithm_directory(self, name:str) -> tuple:
         # C++ or Python algorithm?
         cpp_algo_dir = os.path.join(self.get_plugins_directory(), "C++", name)
         python_algo_dir = os.path.join(self.get_plugins_directory(), "Python", name)
@@ -370,7 +371,7 @@ def _download_algorithm(self, name:str, public_hub: bool = True, private_hub: bo
 
         return algo_info, language, target_dir
 
-    def _find_best_package_url(self, packages:list):
+    def _find_best_package_url(self, packages: list) -> str:
         """
         At this point, plugins are already filtered to ensure compatibility.
         So for now, we just want to pick the latest version
@@ -392,7 +393,7 @@ def _find_best_package_url(self, packages:list):
         return url
 
     @staticmethod
-    def _check_compatibility(algo):
+    def _check_compatibility(algo: dict) -> bool:
         language = utils.ApiLanguage.PYTHON if algo["language"] == "PYTHON" else utils.ApiLanguage.CPP
         packages = algo["packages"]
 
@@ -403,7 +404,7 @@ def _check_compatibility(algo):
         return False
 
     @staticmethod
-    def _check_package_compatibility(package, language):
+    def _check_package_compatibility(package: dict, language: utils.ApiLanguage) -> bool:
         if (
                 IkomiaRegistry._check_os_compatibility(package) and
                 IkomiaRegistry._check_ikomia_compatibility(package, language) and
@@ -415,7 +416,7 @@ def _check_package_compatibility(package, language):
             return False
 
     @staticmethod
-    def _check_os_compatibility(package):
+    def _check_os_compatibility(package: dict) -> bool:
         current_os = None
         if sys.platform == "win32":
             current_os = "WINDOWS"
@@ -428,14 +429,14 @@ def _check_os_compatibility(package):
         return current_os in os_list
 
     @staticmethod
-    def _check_ikomia_compatibility(package, language):
+    def _check_ikomia_compatibility(package: dict, language: utils.ApiLanguage) -> bool:
         ikomia_condition = package["platform"]["ikomia"]
         min_version, max_version = IkomiaRegistry._split_min_max_version(ikomia_condition)
         state = utils.get_compatibility_state(min_version, max_version, language)
         return state == utils.PluginState.VALID
 
     @staticmethod
-    def _check_python_compatibility(package):
+    def _check_python_compatibility(package: dict) -> bool:
         current_version = platform.python_version()
         min_version, max_version = IkomiaRegistry._split_min_max_version(package["platform"]["python"])
         current_sem_ver = semver.Version.parse(current_version)
@@ -452,7 +453,7 @@ def _check_python_compatibility(package):
         return True
 
     @staticmethod
-    def _check_architecture(package, language):
+    def _check_architecture(package: dict, language: utils.ApiLanguage) -> bool:
         if language == utils.ApiLanguage.CPP:
             # TODO: test it!
             # Check CPU architecture
@@ -500,7 +501,7 @@ def _check_installed_modules(self, algo_dir:str):
                     utils.plugintools.uninstall_package(line.rstrip())
 
     @staticmethod
-    def _split_min_max_version(version: str):
+    def _split_min_max_version(version: str) -> str:
         match = re.search(r">=(\d+\.\d+\.?\d*),?<?(\d*\.?\d*\.?\d*)", version)
         if match:
             return match.group(1), match.group(2)
diff --git a/ikomia/dataprocess/tile_processing.py b/ikomia/dataprocess/tile_processing.py
index 02be2705..38dabad4 100644
--- a/ikomia/dataprocess/tile_processing.py
+++ b/ikomia/dataprocess/tile_processing.py
@@ -16,9 +16,9 @@
 import math
 
 
-def tile_process(img, tile_size, overlap_ratio, sf, size_divisible_by, minimum_size, process):
+def tile_process(img: np.ndarray, tile_size: int, overlap_ratio: float, sf: float, size_divisible_by: int,
+                 minimum_size: int, process) -> np.ndarray:
     """
-
     :param img: image (numpy array) to process tile by tile
     :param tile_size: tile size in pixels
     :param overlap_ratio: overlap between 2 tiles in percentage
@@ -36,8 +36,7 @@ def tile_process(img, tile_size, overlap_ratio, sf, size_divisible_by, minimum_s
     elif len(shape) == 2:
         img_up = np.zeros((sf * h, sf * w), dtype='half')
     else:
-        print("Not an image")
-        return
+        raise RuntimeError("Not an image")
 
     img = img[:h, :w]
     tile_h = min(tile_size, h)
diff --git a/ikomia/dataprocess/workflow.py b/ikomia/dataprocess/workflow.py
index e06ffef3..b1b2aedc 100644
--- a/ikomia/dataprocess/workflow.py
+++ b/ikomia/dataprocess/workflow.py
@@ -21,15 +21,20 @@
 import logging
 import enum
 import datetime
+import numpy as np
 import ikomia
-from ikomia import utils, core, dataprocess, dataio
-from ikomia.core import config, task, IODataType
+from ikomia import utils
+from ikomia.core import config, task, IODataType, CWorkflowTask, CWorkflowTaskIO
+from ikomia.dataio import CDataImageIO, CDataVideoIO
+from ikomia.dataprocess import CWorkflow, CImageIO, CVideoIO, CPathIO
+from ikomia.dataprocess.registry import IkomiaRegistry
 from urllib.parse import urlparse
+from typing import Optional, Union
 
 logger = logging.getLogger(__name__)
 
 
-class Workflow(dataprocess.CWorkflow):
+class Workflow(CWorkflow):
     """
     Workflow management of Computer Vision tasks. Implement features to create, modify and run graph-based pipeline of
     :py:class:`~ikomia.core.pycore.CWorkflowTask` objects or derived. Workflows can be created from scratch
@@ -42,16 +47,16 @@ class RunMode(enum.Enum):
         SINGLE = 1
         DIRECTORY = 2
 
-    def __init__(self, name="Untitled", registry=ikomia.ik_registry):
+    def __init__(self, name: str ="Untitled", registry: IkomiaRegistry = ikomia.ik_registry):
         """
         Construct Workflow object with the given name and an :py:class:`~ikomia.dataprocess.registry.IkomiaRegistry`
         object. The latter is used to instanciate algorithm from their unique name when added to the workflow. Thus,
         you are able to use any Ikomia algorithms (built-in and Ikomia HUB) in your workflow.
         """
         if registry is None:
-            dataprocess.CWorkflow.__init__(self, name)
+            CWorkflow.__init__(self, name)
         else:
-            dataprocess.CWorkflow.__init__(self, name, registry)
+            CWorkflow.__init__(self, name, registry)
 
         self.registry = registry
         self.output_folder = os.path.join(config.main_cfg["workflow"]["path"], self.name) + os.sep
@@ -68,34 +73,36 @@ def root(self):
         """
         return self.get_task(self.get_root_id())
 
-    def set_image_input(self, array=None, path="", url="", index=-1, datatype=core.IODataType.IMAGE):
+    def set_image_input(self, array: np.ndarray = None, path: str ="", url: str = "", index: int = -1,
+                        datatype: IODataType = IODataType.IMAGE):
         """
         Set image as global input of the workflow. Image can be specified by a Numpy array, a path or an URL thanks to
         keyword arguments, you have to choose one of them.
 
         Args:
-            array: image input as Numpy array
+            array (ndarray): image input as Numpy array
             path (str): image input as file path (valid formats are those managed by OpenCV)
             url (str): valid URL to image file (valid formats are those managed by OpenCV)
             index (int): zero-based input index, if -1 a new input is added
             datatype (:py:class:`~ikomia.core.pycore.IODataType`): image type
         """
         if array is not None:
-            img_input = dataprocess.CImageIO(datatype, array, "")
+            img_input = CImageIO(datatype, array, "")
         elif path:
-            img_input = dataprocess.CImageIO(datatype, "", path)
+            img_input = CImageIO(datatype, "", path)
         elif url:
             parsed = urlparse(url)
             img_path = config.main_cfg["data"]["path"] + os.path.basename(parsed.path)
             utils.http.download_file(url, img_path)
-            img_input = dataprocess.CImageIO(datatype, "Image", img_path)
+            img_input = CImageIO(datatype, "Image", img_path)
 
         if index == -1 or index >= self.get_input_count():
             self.add_input(img_input)
         else:
             self.set_input(img_input, index, True)
 
-    def set_video_input(self, path="", url="", index=-1, datatype=core.IODataType.VIDEO):
+    def set_video_input(self, path: str = "", url: str = "", index: int = -1,
+                        datatype: IODataType = IODataType.VIDEO):
         """
         Set video as global input of the workflow. Video can be specified by a path or an URL thanks to
         keyword arguments, you have to choose one of them.
@@ -107,19 +114,19 @@ def set_video_input(self, path="", url="", index=-1, datatype=core.IODataType.VI
             datatype (:py:class:`~ikomia.core.pycore.IODataType`): image type
         """
         if path:
-            video_input = dataprocess.CVideoIO(datatype, "", path)
+            video_input = CVideoIO(datatype, "", path)
         elif url:
             parsed = urlparse(url)
             video_path = config.main_cfg["data"]["path"] + os.path.basename(parsed.path)
             utils.http.download_file(url, video_path)
-            video_input = dataprocess.CVideoIO(datatype, "Video", video_path)
+            video_input = CVideoIO(datatype, "Video", video_path)
 
         if index == -1 or index >= self.get_input_count():
             self.add_input(video_input)
         else:
             self.set_input(video_input, index, True)
 
-    def set_directory_input(self, folder="", index=-1):
+    def set_directory_input(self, folder: str = "", index: int = -1):
         """
         Set folder as global input of the workflow. For image-based workflows, all images inside the directory
         (recursively) will be processed.
@@ -132,13 +139,13 @@ def set_directory_input(self, folder="", index=-1):
             logger.error("Directory input not set: you must pass an existing directory.")
             return
 
-        dir_input = dataprocess.CPathIO(core.IODataType.FOLDER_PATH, folder)
+        dir_input = CPathIO(IODataType.FOLDER_PATH, folder)
         if index == -1 or index >= self.get_input_count():
             self.add_input(dir_input)
         else:
             self.set_input(dir_input, index, True)
 
-    def set_parameters(self, params: dict, task_obj=None, task_name="", index=-1):
+    def set_parameters(self, params: dict, task_obj: CWorkflowTask = None, task_name: str = "", index: int = -1):
         """
         Set task parameters as a simple key-value dict.
         You can get parameters keys for each by calling:
@@ -156,6 +163,9 @@ def set_parameters(self, params: dict, task_obj=None, task_name="", index=-1):
         if task_obj is None and not task_name:
             raise RuntimeError("Unable to set task parameters: parameters must include either a valid name or task instance.")
 
+        # Ensure parameter values are string due to C++ typing constraint
+        params = task.conform_parameters(params)
+
         if task_obj is not None:
             task_obj.set_parameters(params)
         elif task_name:
@@ -172,7 +182,25 @@ def set_parameters(self, params: dict, task_obj=None, task_name="", index=-1):
                 elif 0 <= index < len(task_obj):
                     task_obj[index].set_parameters(params)
 
-    def get_time_metrics(self):
+    def set_workflow_parameters(self, params: dict):
+        """
+        Set workflow parameters. Available parameters are those exposed when workflow is saved.
+        Actually, an exposed parameter is bound to a task parameter within a workflow.
+        The aim is to be able to select meaningfull parameters with respect to the workflow objective.
+
+        Args:
+            params (dict): key-value pairs
+        """
+        try:
+            params = task.conform_parameters(params)
+            for name in params:
+                self.set_exposed_parameter(name, params[name])
+        except RuntimeError as e:
+            available_params = self.get_exposed_parameters()
+            logger.error(e)
+            logger.error(f"Available parameters: {available_params}")
+
+    def get_time_metrics(self) -> dict:
         """
         Get metrics around workflow execution time. This includes the total execution time of the workflow, and for
         each task, the execution time and the execution time from the start.
@@ -190,7 +218,8 @@ def get_time_metrics(self):
 
         return metrics
 
-    def get_task_output(self, task_obj=None, task_name="", task_index=0, types=[IODataType.IMAGE], output_index=-1):
+    def get_task_output(self, task_obj=None, task_name: str = "", task_index: int = 0, types: list = [IODataType.IMAGE],
+                        output_index: int = -1) -> CWorkflowTaskIO:
         """
         Get specific output(s) defined by their types (:py:class:`~ikomia.core.PyCore.IODataType`) for the given task.
 
@@ -228,7 +257,7 @@ def get_task_output(self, task_obj=None, task_name="", task_index=0, types=[IODa
         else:
             return task.get_output(task_obj, types, output_index)
 
-    def get_tasks(self):
+    def get_tasks(self) -> list:
         """
         Get all tasks composing the workflow.
 
@@ -244,9 +273,9 @@ def get_tasks(self):
 
         return tasks
 
-    def get_task_id(self, task):
+    def get_task_id(self, task) -> int:
         """
-        Get task unique identifier from the task instance/
+        Get task unique identifier from the task instance.
 
         Args:
             task (:py:class:`~ikomia.core.pycore.CWorkflowTask` based object): task instance
@@ -256,8 +285,11 @@ def get_task_id(self, task):
         """
         return self.task_to_id[task.uuid]
 
-    def add_task(self, task=None, name:str="", params:dict={}, auto_connect:bool=False,
-                 public_hub:bool=True, private_hub=False):
+    def get_workflow_parameters(self):
+        return self.get_exposed_parameters()
+
+    def add_task(self, task: CWorkflowTask = None, name: str = "", params: dict = {}, auto_connect: bool=False,
+                 public_hub: bool = True, private_hub: bool = False) -> CWorkflowTask:
         """
         Add task identified by its unique name in the workflow. If the given task is not yet in the registry, it will be
         firstly downloaded and installed from Ikomia HUB. Task unique identifier can then be retrieved with
@@ -297,7 +329,7 @@ def add_task(self, task=None, name:str="", params:dict={}, auto_connect:bool=Fal
 
         return task
 
-    def remove_task(self, task=None, name: str="", index: int=0):
+    def remove_task(self, task: CWorkflowTask = None, name: str = "", index: int = 0):
         """
         Remove task from workflow specified by task instance or name (with corresponding index).
 
@@ -317,7 +349,7 @@ def remove_task(self, task=None, name: str="", index: int=0):
         super().delete_task(self.task_to_id[task.uuid])
         del self.task_to_id[task.uuid]
 
-    def find_task(self, name: str, index=-1):
+    def find_task(self, name: str, index: int = -1) -> Union[CWorkflowTask, list]:
         """
         Get identifiers and instance of tasks with the given name in the workflow.
 
@@ -342,7 +374,7 @@ def find_task(self, name: str, index=-1):
         else:
             return tasks
 
-    def connect_tasks(self, src, target, edges=None):
+    def connect_tasks(self, src: CWorkflowTask, target: CWorkflowTask, edges: list = []):
         """
         Connect two tasks of the workflow. Depending of the inputs/outputs configuration, multiple connections between
         the two tasks can be set. A connection is a pair (ie tuple) composed by the output index of the source task
@@ -351,7 +383,7 @@ def connect_tasks(self, src, target, edges=None):
         Args:
             src (:py:class:`~ikomia.core.pycore.CWorkflowTask` based object): source task or None (connect to root)
             target (:py:class:`~ikomia.core.pycore.CWorkflowTask` based object): target task
-            edges (list of pair): connections. If *None* is passed, auto-connection is enabled so that the system will try to find the best connections automatically with respect to inputs and outputs data types.
+            edges (list of pair): connections. If empty list is passed, auto-connection is enabled so that the system will try to find the best connections automatically with respect to inputs and outputs data types.
         """
         if src.uuid == self.root_uuid:
             src_id = self.get_root_id()
@@ -360,7 +392,7 @@ def connect_tasks(self, src, target, edges=None):
 
         target_id = self.get_task_id(target)
 
-        if edges is None:
+        if len(edges) == 0:
             self.connect(src_id, target_id, -1, -1)
         else:
             for edge in edges:
@@ -391,7 +423,7 @@ def run(self):
 
         logger.info(f"Workflow {self.name} run successfully in {total_time} ms.")
 
-    def run_on(self, array=None, path="", url="", folder=""):
+    def run_on(self, array: np.ndarray = None, path: str = "", url: str = "", folder: str = ""):
         """
         Convenient function to run the workflow on common inputs. For more advanced use, please consult
         :py:class:`~ikomia.dataprocess.workflow.Workflow`.
@@ -424,7 +456,13 @@ def run_on(self, array=None, path="", url="", folder=""):
 
             self.run()
 
-    def load(self, path):
+    def load(self, path: str):
+        """
+        Load the worflow file at the given path. The function will try to install algorithms if they are not available.
+
+        Args:
+            path (str): full path to the workflow definition file to load.
+        """
         prepare_runtime_env(path)
         super().load(path)
 
@@ -436,10 +474,83 @@ def load(self, path):
             task = self.get_task(task_id)
             self.task_to_id[task.uuid] = task_id
 
+    def save(self, path: str, exposed_params: Optional[dict] = None, exposed_outputs: Optional[dict] = None):
+        """
+        Save workflow into a JSON definition file.
+
+        Args:
+            path (str): full path to the workflow definition file to save.
+            exposed_params (dict): set the list of task parameters that has to be exposed at workflow level. The aim is
+                to be able to select meaningfull parameters with respect to the workflow objective.
+
+                Example of the dict structure:
+
+                .. code-block:: python
+
+                    exposed_params = {
+                        "task_name": {
+                            "task_parameter_name_1": {
+                                "name": "my_workflow_parameter_name_1",
+                                "description": "my_description_1",
+                            },
+                            "task_parameter_name_2": {
+                                "name": "my_workflow_parameter_name_2",
+                                "description": "my_description_2",
+                            },
+                        }
+                    }
+
+                To specify a task, you can give a task name, a task instance or a task id. Be careful with task name
+                because, in case of workflow involving several times the same task, the function will expose parameters
+                of the first task only. Prefer using task instance or task id in this case.
+
+                If 'name" field is empty, the original task parameter name is used.
+
+                The 'description' field is also optional.
+
+                If you want to expose all parameters of a task, just pass an empty dict for the value of the
+                task name key.
+            exposed_outputs (dict): set the list of outputs that has to be exposed at workflow level. The aim is
+                to be able to select meaningfull outputs with respect to the workflow objective.
+
+                Example of the dict structure:
+
+                .. code-block:: python
+
+                    exposed_outputs = {
+                        "task_name": [
+                            {
+                                "description": "my_description_1",
+                                "index": 0,
+                            },
+                            {
+                                "description": "my_description_2",
+                                "index": 1,
+                            },
+                        ]
+                    }
+
+                To specify a task, you can give a task name, a task instance or a task id. Be careful with task name
+                because, in case of workflow involving several times the same task, the function will expose parameters
+                of the first task only. Prefer using task instance or task id in this case.
+
+                The 'description' field is optional.
+
+                If you want to expose all outputs of a task, just pass an empty list for the value of the
+                task name key.
+        """
+        if exposed_params:
+            self._expose_parameters(exposed_params)
+
+        if exposed_outputs:
+            self._expose_outputs(exposed_outputs)
+
+        super().save(path)
+
     def _run_directory(self):
         for i in range(self.get_input_count()):
             input_type = self.get_input_data_type(i)
-            if input_type == core.IODataType.FOLDER_PATH:
+            if input_type == IODataType.FOLDER_PATH:
                 dir_input = self.get_input(i)
 
                 for root, subdirs, files in os.walk(dir_input.get_path(), topdown=True):
@@ -470,43 +581,103 @@ def _get_run_mode(self):
 
         target_types = self.get_root_target_types()
 
-        if core.IODataType.FOLDER_PATH in input_types and core.IODataType.FOLDER_PATH not in target_types:
+        if IODataType.FOLDER_PATH in input_types and IODataType.FOLDER_PATH not in target_types:
             return Workflow.RunMode.DIRECTORY
         else:
             return Workflow.RunMode.SINGLE
 
     @staticmethod
-    def _check_run_input(array, path, url, folder):
+    def _check_run_input(array: np.ndarray, path: str, url: str, folder: str) -> bool:
         return array is not None or path or url or folder
 
     @staticmethod
-    def _is_image_input(array=None, path="", url=""):
+    def _is_image_input(array: np.ndarray = None, path: str = "", url: str = "") -> bool:
         if array is not None:
             return True
         elif path:
             filename, ext = os.path.splitext(path)
-            return dataio.CDataImageIO.is_image_format(ext)
+            return CDataImageIO.is_image_format(ext)
         elif url:
             parsed_url = urlparse(url)
             filename, ext = os.path.splitext(parsed_url.path)
-            return dataio.CDataImageIO.is_image_format(ext)
+            return CDataImageIO.is_image_format(ext)
         else:
             return False
 
     @staticmethod
-    def _is_video_input(path="", url=""):
+    def _is_video_input(path: str = "", url: str = "") -> bool:
         if path:
             filename, ext = os.path.splitext(path)
-            return dataio.CDataVideoIO.is_video_format(ext, True)
+            return CDataVideoIO.is_video_format(ext, True)
         elif url:
             parsed_url = urlparse(url)
             filename, ext = os.path.splitext(parsed_url.path)
-            return dataio.CDataVideoIO.is_video_format(ext, True)
+            return CDataVideoIO.is_video_format(ext, True)
         else:
             return False
 
+    def _expose_parameters(self, exposed_params: dict):
+        self.clear_exposed_parameters()
+
+        for task_key in exposed_params:
+            if type(task_key) is CWorkflowTask:
+                task_id = self.task_to_id[task_key.uuid]
+            elif type(task_key) is int:
+                task_id = task_key
+            elif type(task_key) is str:
+                # TODO: how to manage multiple tasks with the same name
+                task = self.find_task(task_key, 0)
+                task_id = self.task_to_id[task.uuid]
+            else:
+                raise TypeError("Task identification must be either a task instance, a task id or a task name.")
+
+            if len(exposed_params[task_key]) == 0:
+                # Expose all parameters
+                t = self.get_task(task_id)
+                task_params = t.get_parameters()
+
+                for param in task_params:
+                    self.add_exposed_parameter("", "", task_id, param)
+            else:
+                # Expose specified parameters only
+                for param_name, info in exposed_params[task_key].items():
+                    if type(param_name) is not str:
+                        raise TypeError("String expected for task parameter name.")
+
+                    name = info["name"] if "name" in info else ""
+                    description = info["description"] if "description" in info else ""
+                    self.add_exposed_parameter(name, description, task_id, param_name)
+
+    def _expose_outputs(self, outputs: dict):
+        self.clear_outputs()
+
+        for task_key in outputs:
+            if type(task_key) is CWorkflowTask:
+                task_id = self.task_to_id[task_key.uuid]
+            elif type(task_key) is int:
+                task_id = task_key
+            elif type(task_key) is str:
+                # TODO: how to manage multiple tasks with the same name
+                task = self.find_task(task_key, 0)
+                task_id = self.task_to_id[task.uuid]
+            else:
+                raise TypeError("Task identification must be either a task instance, a task id or a task name.")
+
+            if len(outputs[task_key]) == 0:
+                # Expose all outputs
+                t = self.get_task(task_id)
+                task_outputs = t.get_outputs()
+
+                for i, output in enumerate(task_outputs):
+                    self.add_output("", task_id, i)
+            else:
+                # Expose specified output only
+                for output_info in outputs[task_key]:
+                    description = output_info["description"] if "description" in output_info else ""
+                    self.add_output(description, task_id, output_info["index"])
+
 
-def create(name="untitled"):
+def create(name: str = "untitled"):
     """
     Create new empty workflow.
     See also :py:meth:`~ikomia.dataprocess.workflow.Workflow.add_task`,
@@ -519,7 +690,7 @@ def create(name="untitled"):
     return Workflow(name, ikomia.ik_registry)
 
 
-def load(path):
+def load(path: str) -> Workflow:
     """
     Load Ikomia workflow from the given path.
 
@@ -557,7 +728,7 @@ def prepare_runtime_env(workflow_path:str):
                 raise RuntimeError(f"Workflow preparation failed at task {t} for the following reason: {e}")
 
 
-def install_requirements(path):
+def install_requirements(path: str) -> bool:
     """
     Install Python requirements from all algorithms of the workflow stored at the given path.
     Algorithms must be installed locally before calling this function.
diff --git a/ikomia/dnn/dataset.py b/ikomia/dnn/dataset.py
index e23690f3..1cc2123a 100644
--- a/ikomia/dnn/dataset.py
+++ b/ikomia/dnn/dataset.py
@@ -18,7 +18,7 @@
 
 import os
 import json
-from ikomia import core
+from ikomia.core import CPointF, CGraphicsPolygon, CGraphicsConversion
 from PIL import Image
 from collections import defaultdict
 import xml.etree.ElementTree as ET
@@ -39,7 +39,7 @@
                      ".ppm", ".pxm", ".pnm", ".sr", ".ras", ".exr", ".hdr", ".pic"]
 
 
-def load_via_dataset(path):
+def load_via_dataset(path: str) -> dict:
     """
     Load VGG Image Annotator (VIA) dataset.
     VIA dataset is a single JSON file containing all information.
@@ -131,7 +131,7 @@ def load_via_dataset(path):
     return data
 
 
-def read_class_names(txt_path):
+def read_class_names(txt_path: str) -> list:
     """
     Helper function to parse text file and extract class names.
     The text file must be structured so that one line = one class name.
@@ -148,7 +148,7 @@ def read_class_names(txt_path):
     return classes
 
 
-def load_yolo_dataset(folder_path, class_path):
+def load_yolo_dataset(folder_path: str, class_path: str) -> dict:
     """
     Load YOLO dataset.
     YOLO dataset consists of a list of text files with
@@ -249,28 +249,30 @@ def load_yolo_dataset(folder_path, class_path):
     return data
 
 
-def ann_to_rle(img, ann):
+def ann_to_rle(img: np.ndarray, ann: dict) -> np.ndarray:
     """
     Convert annotation which can be polygons, uncompressed RLE to RLE.
     :return: binary mask (numpy 2D array)
     """
     h, w = img['height'], img['width']
     segm = ann['segmentation']
-    if type(segm) == list:
+    if type(segm) is list:
         # polygon -- a single object might consist of multiple parts
         # we merge all parts into one mask rle code
         rles = maskUtils.frPyObjects(segm, h, w)
         rle = maskUtils.merge(rles)
-    elif type(segm['counts']) == list:
+    elif type(segm['counts']) is list:
         # uncompressed RLE
         rle = maskUtils.frPyObjects(segm, h, w)
     else:
         # rle
         rle = ann['segmentation']
+
     return rle
 
 
-def load_coco_dataset(path, image_folder, task="instance_segmentation", output_folder=""):
+def load_coco_dataset(path: str, image_folder: str,
+                      task: str = "instance_segmentation", output_folder: str = "") -> dict:
     """
     Load COCO dataset (2017 version).
     COCO dataset consists in a JSON file describing annotations
@@ -381,7 +383,7 @@ def load_coco_dataset(path, image_folder, task="instance_segmentation", output_f
     return data
 
 
-def load_pascalvoc_dataset(annotation_folder, img_folder, instance_seg_folder, class_path):
+def load_pascalvoc_dataset(annotation_folder: str, img_folder: str, instance_seg_folder: str, class_path: str) -> dict:
     """
     Load PASCAL-VOC dataset (version 2012).
     PASCAL-VOC dataset is structured in different folders:
@@ -477,7 +479,7 @@ def load_pascalvoc_dataset(annotation_folder, img_folder, instance_seg_folder, c
     return data
 
 
-def polygon_to_mask(polygons, width, height):
+def polygon_to_mask(polygons: list, width: int, height: int) -> np.ndarray:
     """
     Helper function to generate image mask from a list of polygons.
     The function assumes that each polygon is a list of xy coordinates.
@@ -504,11 +506,11 @@ def polygon_to_mask(polygons, width, height):
             if y > height - 1:
                 y = height - 1
 
-            pts.append(core.CPointF(x, y))
+            pts.append(CPointF(x, y))
             i += 2
 
-        graphics_poly = core.CGraphicsPolygon(pts)
+        graphics_poly = CGraphicsPolygon(pts)
         graphics.append(graphics_poly)
 
-    conversion = core.CGraphicsConversion(width, height)
+    conversion = CGraphicsConversion(width, height)
     return conversion.graphics_to_binary_mask(graphics)
diff --git a/ikomia/dnn/datasetio.py b/ikomia/dnn/datasetio.py
index d6ec6452..82fb7ecc 100644
--- a/ikomia/dnn/datasetio.py
+++ b/ikomia/dnn/datasetio.py
@@ -18,12 +18,13 @@
 Derived from :py:class:`~ikomia.dataprocess.pydataprocess.CDatasetIO`.
 """
 
-from ikomia import core, dataprocess
+from ikomia import core
+from ikomia.dataprocess import CDatasetIO
 import random
 import json
 
 
-class IkDatasetIO(dataprocess.CDatasetIO):
+class IkDatasetIO(CDatasetIO):
     """
         Define task input or output containing deep learning dataset structure.
         Derived from :py:class:`~ikomia.dataprocess.pydataprocess.CDatasetIO`.
@@ -60,20 +61,20 @@ class IkDatasetIO(dataprocess.CDatasetIO):
                 - keypoint_connection_rules (list[tuple(str, str, (r,g,b))]): each tuple specifies a pair of connected keypoints and the color to use for the line between them.
     """
 
-    def __init__(self, format="other"):
+    def __init__(self, format: str = "other"):
         """
         Constructor
 
         Args:
             format (str): dataset source format.
         """
-        dataprocess.CDatasetIO.__init__(self, format)
+        CDatasetIO.__init__(self, format)
         # Data are stored into a dict
         self.data = {}
         self.category_colors = {}
         self.has_bckgnd_class = False
 
-    def get_image_paths(self):
+    def get_image_paths(self) -> list:
         """
         Return the list of all images path contained in the dataset.
 
@@ -87,12 +88,12 @@ def get_image_paths(self):
 
         return paths
 
-    def get_categories(self):
+    def get_categories(self) -> dict:
         """
         Return the list of categories (ie instance classes) in the dataset.
 
         Returns:
-            :py:class:`~ikomia.dataprocess.pydataprocess.MapIntStr` list: categories (dict-like structure)
+            categories (dict)
         """
         categories = {}
         for category_id in self.data["metadata"]["category_names"]:
@@ -100,7 +101,7 @@ def get_categories(self):
 
         return categories
 
-    def get_category_count(self):
+    def get_category_count(self) -> int:
         """
         Return the number of categories (ie instance classes) in the dataset.
 
@@ -114,7 +115,7 @@ def get_category_count(self):
 
         return category_max + 1
 
-    def get_mask_path(self, img_path):
+    def get_mask_path(self, img_path: str) -> str:
         """
         Return the file path of the segmentation mask for the given image.
 
@@ -138,7 +139,7 @@ def get_mask_path(self, img_path):
 
         return ""
 
-    def get_graphics_annotations(self, img_path):
+    def get_graphics_annotations(self, img_path: str) -> list:
         """
         Return a list of Ikomia graphics items corresponding
         to the annotations of a given image (bounding box, polygons).
@@ -231,7 +232,7 @@ def get_graphics_annotations(self, img_path):
 
         return graphics
 
-    def is_data_available(self):
+    def is_data_available(self) -> bool:
         """
         Check whether the dataset structure contains data.
 
@@ -249,7 +250,7 @@ def clear_data(self):
         """
         self.data.clear()
 
-    def _get_random_category_colors(self):
+    def _get_random_category_colors(self) -> dict:
         random.seed(1)
         colors = {}
         for categ_id in self.data["metadata"]["category_names"]:
@@ -257,7 +258,7 @@ def _get_random_category_colors(self):
             colors[categ_id] = color
         return colors
 
-    def save(self, path):
+    def save(self, path: str):
         """
         Save dataset structure as JSON.
 
@@ -267,7 +268,7 @@ def save(self, path):
         with open(path, "w") as outfile:
             json.dump(self.data, outfile)
 
-    def load(self, path):
+    def load(self, path: str):
         """
         Load JSON as dataset structure.
 
@@ -280,8 +281,8 @@ def load(self, path):
                 self.data["metadata"]["category_names"] = {int(k): v for k, v in
                                                            self.data["metadata"]["category_names"].items()}
 
-    def to_json(self, options=[]):
+    def to_json(self, options: list = []) -> str:
         return json.dumps(self.data)
 
-    def from_json(self, json_string):
+    def from_json(self, json_string: str):
         self.data = json.loads(json_string)
diff --git a/ikomia/dnn/dnntrain.py b/ikomia/dnn/dnntrain.py
index 36a4d7b5..d8738a30 100644
--- a/ikomia/dnn/dnntrain.py
+++ b/ikomia/dnn/dnntrain.py
@@ -17,7 +17,8 @@
 """
 import logging
 import mlflow
-from ikomia.core import config
+from typing import Optional
+from ikomia.core import config, CWorkflowTaskParam
 from ikomia.dataprocess import CDnnTrainTask
 from ikomia.dnn import datasetio, monitoring
 from datetime import datetime
@@ -40,7 +41,7 @@ class TrainProcess(CDnnTrainTask):
     It must be used with :py:class:`~ikomia.core.task.TaskParam` or derived for parameters.
     Derived from :py:class:`~ikomia.dataprocess.pydataprocess.CDnnTrainTask`.
     """
-    def __init__(self, name, param):
+    def __init__(self, name: str, param: CWorkflowTaskParam):
         """
         Constructor. Initialize mlflow local server.
 
@@ -72,7 +73,7 @@ def _create_mlflow_experiment(self):
             logger.warning("Unable to create MLFlow experiment. Please check for server startup errors.")
             logger.debug(e)
 
-    def _is_experiment_exists(self):
+    def _is_experiment_exists(self) -> bool:
         try:
             current_exp = mlflow.get_experiment(str(self.experiment_id))
             return current_exp.lifecycle_stage == "active"
@@ -109,7 +110,7 @@ def begin_task_run(self):
         if param is not None:
             self.log_params(param.cfg)
 
-    def log_param(self, key, value):
+    def log_param(self, key: str, value: any):
         """
         Log parameter to mlflow server
 
@@ -120,7 +121,7 @@ def log_param(self, key, value):
         if self.experiment_id is not None:
             mlflow.log_param(key, value)
 
-    def log_params(self, params):
+    def log_params(self, params: dict):
         """
         Log parameters to mlflow server
 
@@ -130,7 +131,7 @@ def log_params(self, params):
         if self.experiment_id is not None:
             mlflow.log_params(params)
 
-    def log_metric(self, key, value, step=None):
+    def log_metric(self, key: str, value: any, step: Optional[int] = None):
         """
         Log metric to mlflow server
 
@@ -142,7 +143,7 @@ def log_metric(self, key, value, step=None):
         if self.experiment_id is not None:
             mlflow.log_metric(key, value, step)
 
-    def log_metrics(self, metrics, step=None):
+    def log_metrics(self, metrics: dict, step: Optional[int] = None):
         """
         Log metrics to mlflow server
 
@@ -153,7 +154,7 @@ def log_metrics(self, metrics, step=None):
         if self.experiment_id is not None:
             mlflow.log_metrics(metrics, step)
 
-    def log_artifact(self, file_path):
+    def log_artifact(self, file_path: str):
         """
         Log artifact to mlflow server.
         Artifact could be any file (model weights, configuration file...)
@@ -164,7 +165,7 @@ def log_artifact(self, file_path):
         if self.experiment_id is not None:
             mlflow.log_artifact(file_path)
 
-    def log_artifacts(self, folder_path):
+    def log_artifacts(self, folder_path: str):
         """
         Log artifacts to mlflow server.
         Artifacts could be any files stored in the same folder (model weights, configuration file...)
diff --git a/ikomia/dnn/torch/datasetmapper.py b/ikomia/dnn/torch/datasetmapper.py
index e7a5cfeb..97d80cf9 100644
--- a/ikomia/dnn/torch/datasetmapper.py
+++ b/ikomia/dnn/torch/datasetmapper.py
@@ -32,7 +32,7 @@ class TorchDatasetMapper(Dataset):
     Class derived from torch.utils.data.Dataset.
     """
 
-    def __init__(self, ik_dataset, has_bckgnd_class, transforms):
+    def __init__(self, ik_dataset: dict, has_bckgnd_class: bool, transforms):
         self.ik_dataset = ik_dataset
         self.has_bckgnd_class = has_bckgnd_class
         self.transforms = transforms
@@ -40,7 +40,7 @@ def __init__(self, ik_dataset, has_bckgnd_class, transforms):
     def __len__(self):
         return len(self.ik_dataset["images"])
 
-    def __getitem__(self, idx):
+    def __getitem__(self, idx: int):
         img_data = self.ik_dataset["images"][idx]
         # Load image
         img = Image.open(img_data["filename"]).convert("RGB")
@@ -113,7 +113,8 @@ def __getitem__(self, idx):
 
         return img, target
 
-    def _load_instance_seg_masks(self, path):
+    @staticmethod
+    def _load_instance_seg_masks(path: str):
         # Load mask as [N H W] numpy array
         filename, extension = os.path.splitext(path)
 
@@ -139,7 +140,8 @@ def _load_instance_seg_masks(self, path):
             print("Error: segmentation mask format not supported")
             return None
 
-    def _load_semantic_seg_masks(self, path):
+    @staticmethod
+    def _load_semantic_seg_masks(path: str):
         # Load mask as [N H W] numpy array
         filename, extension = os.path.splitext(path)
 
diff --git a/ikomia/dnn/torch/models.py b/ikomia/dnn/torch/models.py
index e8160b63..97d493f0 100644
--- a/ikomia/dnn/torch/models.py
+++ b/ikomia/dnn/torch/models.py
@@ -21,9 +21,12 @@
 - `Torchvision object detection and segmentation <https://pytorch.org/docs/stable/torchvision/models.html#object-detection-instance-segmentation-and-person-keypoint-detection>`_
 """
 
+import logging
 import torch
 from torchvision import models
 
+logger = logging.getLogger(__name__)
+
 
 def set_parameter_requires_grad(model, feature_extracting):
     if feature_extracting:
@@ -31,7 +34,8 @@ def set_parameter_requires_grad(model, feature_extracting):
             param.requires_grad = False
 
 
-def resnet(model_name='resnet50', train_mode=False, use_pretrained=False, feature_extract=False, classes=2):
+def resnet(model_name: str = 'resnet50', train_mode: bool = False, use_pretrained: bool = False,
+           feature_extract: bool = False, classes: int = 2):
     """
     Create Torchvision ResNet model for training or inference.
 
@@ -72,11 +76,12 @@ def resnet(model_name='resnet50', train_mode=False, use_pretrained=False, featur
         model_ft.eval()
 
     # Print the model we just instantiated
-    print(model_ft)
+    logger.debug(model_ft)
     return model_ft
 
 
-def resnext(model_name='resnext50', train_mode=False, use_pretrained=False, feature_extract=False, classes=2):
+def resnext(model_name: str = 'resnext50', train_mode: bool = False, use_pretrained: bool = False,
+            feature_extract: bool = False, classes: int = 2):
     """
     Create Torchvision ResNeXt model for training or inference.
 
@@ -111,16 +116,15 @@ def resnext(model_name='resnext50', train_mode=False, use_pretrained=False, feat
         model_ft.eval()
 
     # Print the model we just instantiated
-    print(model_ft)
+    logger.debug(model_ft)
     return model_ft
 
 
-def mnasnet(train_mode=False, use_pretrained=False, feature_extract=False, classes=2):
+def mnasnet(train_mode: bool = False, use_pretrained: bool = False, feature_extract: bool = False, classes: int = 2):
     """
     Create Torchvision MnasNet model for training or inference.
 
     Args:
-        model_name (str): model name
         train_mode (boolean): True or False
         use_pretrained (boolean): True to do transfer learning from pre-trained model, False to train from scratch
         feature_extract (boolean): transfer learning only, True to keep pre-trained features (train last layers only), False to train all layers
@@ -142,16 +146,15 @@ def mnasnet(train_mode=False, use_pretrained=False, feature_extract=False, class
         model_ft.eval()
 
     # Print the model we just instantiated
-    print(model_ft)
+    logger.debug(model_ft)
     return model_ft
 
 
-def faster_rcnn(train_mode=False, use_pretrained=True, input_size=800, classes=2):
+def faster_rcnn(train_mode: bool = False, use_pretrained: bool = True, input_size: int = 800, classes: int = 2):
     """
     Create Torchvision Faster RCNN model for training or inference.
 
     Args:
-        model_name (str): model name
         train_mode (boolean): True or False
         use_pretrained (boolean): True to do transfer learning from pre-trained model, False to train from scratch
         input_size (int): input image size
@@ -171,16 +174,15 @@ def faster_rcnn(train_mode=False, use_pretrained=True, input_size=800, classes=2
     if not train_mode:
         model_ft.eval()
 
-    print(model_ft)
+    logger.debug(model_ft)
     return model_ft
 
 
-def mask_rcnn(train_mode=False, use_pretrained=True, input_size=800, classes=2):
+def mask_rcnn(train_mode: bool = False, use_pretrained: bool = True, input_size: int = 800, classes: int = 2):
     """
     Create Torchvision Mask RCNN model for training or inference.
 
     Args:
-        model_name (str): model name
         train_mode (boolean): True or False
         use_pretrained (boolean): True to do transfer learning from pre-trained model, False to train from scratch
         input_size (int): input image size
@@ -205,5 +207,5 @@ def mask_rcnn(train_mode=False, use_pretrained=True, input_size=800, classes=2):
     if not train_mode:
         model_ft.eval()
 
-    print(model_ft)
+    logger.debug(model_ft)
     return model_ft
diff --git a/ikomia/dnn/torch/utils.py b/ikomia/dnn/torch/utils.py
index 0f26f380..66ae2a79 100644
--- a/ikomia/dnn/torch/utils.py
+++ b/ikomia/dnn/torch/utils.py
@@ -15,10 +15,10 @@
 import torch
 
 
-def save_pth(model, path):
+def save_pth(model, path: str):
     torch.save(model.state_dict(), path)
 
 
-def save_onnx(model, input_shape, device, path):
+def save_onnx(model, input_shape, device, path: str):
     dummy_input = torch.randn(input_shape, device=device)
     torch.onnx.export(model, dummy_input, path)
diff --git a/ikomia/utils/__init__.py b/ikomia/utils/__init__.py
index c14f5a84..2e395004 100644
--- a/ikomia/utils/__init__.py
+++ b/ikomia/utils/__init__.py
@@ -25,7 +25,7 @@
 from ikomia.utils.data import *
 
 
-def is_colab():
+def is_colab() -> bool:
     # Is environment a Google Colab instance?
     try:
         import google.colab
@@ -34,7 +34,7 @@ def is_colab():
         return False
 
 
-def strtobool(val):
+def strtobool(val: str) -> bool:
     """Convert a string representation of truth to true (1) or false (0).
     True values are 'y', 'yes', 't', 'true', 'on', and '1'; false values
     are 'n', 'no', 'f', 'false', 'off', and '0'.  Raises ValueError if
diff --git a/ikomia/utils/autocomplete.py b/ikomia/utils/autocomplete.py
index 48058353..02e499f4 100644
--- a/ikomia/utils/autocomplete.py
+++ b/ikomia/utils/autocomplete.py
@@ -34,7 +34,7 @@
 logger = logging.getLogger(__name__)
 
 
-def _write_auto_complete(f, task_name="", task=None, local=True):
+def _write_auto_complete(f, task_name: str = "", task: ikomia.core.CWorkflowTask = None, local: bool = True):
     if not task_name and task is None:
         raise RuntimeError("Auto-completion: parameters must include either a valid name or task instance.")
 
@@ -107,7 +107,7 @@ def _write_auto_complete(f, task_name="", task=None, local=True):
     f.write(f"        return \"{task_name}\"\n\n")
 
 
-def _generate_python_file(folder):
+def _generate_python_file(folder: str):
     ik_file_path = os.path.join(folder, "ik.py")
     with open(ik_file_path, 'w+') as f:
         # Imports
@@ -129,12 +129,12 @@ def _generate_python_file(folder):
         importlib.reload(ikomia.utils.ik)
 
 
-def _is_valid_python_plugin_folder(name:str, folder_path:str):
+def _is_valid_python_plugin_folder(name: str, folder_path: str) -> bool:
     main_python_file = os.path.join(folder_path, f"{name}.py")
     return os.path.exists(main_python_file)
 
 
-def _is_valid_cpp_plugin_folder(name:str, folder_path:str):
+def _is_valid_cpp_plugin_folder(name: str, folder_path: str) -> bool:
     for name in os.listdir(folder_path):
         # Just check if at least one shared library exists -> maybe not sufficient...
         filename, ext = os.path.splitext(name)
@@ -144,7 +144,7 @@ def _is_valid_cpp_plugin_folder(name:str, folder_path:str):
     return False
 
 
-def _check_local_sync():
+def _check_local_sync() -> bool:
     if not _ik_auto_complete:
         return False
 
@@ -185,7 +185,7 @@ def _check_local_sync():
         return True
 
 
-def _check_online_sync():
+def _check_online_sync() -> bool:
     if not _ik_auto_complete:
         return False
 
@@ -214,7 +214,7 @@ def _check_online_sync():
         return False
 
 
-def _check_task_params(task):
+def _check_task_params(task: ikomia.core.CWorkflowTask) -> bool:
     if not _ik_auto_complete:
         return False
 
@@ -234,7 +234,7 @@ def _check_task_params(task):
     return True
 
 
-def _has_local_cache():
+def _has_local_cache() -> bool:
     filename = "autocomplete_local.cache"
     cache_file_path1 = os.path.join(os.path.dirname(__file__), filename)
     local_site = os.path.join(site.getusersitepackages(), "ikomia", "utils")
@@ -242,7 +242,7 @@ def _has_local_cache():
     return os.path.isfile(cache_file_path1) or os.path.isfile(cache_file_path2)
 
 
-def _has_online_cache():
+def _has_online_cache() -> bool:
     filename = "autocomplete_online.cache"
     cache_file_path1 = os.path.join(os.path.dirname(__file__), filename)
     local_site = os.path.join(site.getusersitepackages(), "ikomia", "utils")
@@ -250,7 +250,7 @@ def _has_online_cache():
     return os.path.isfile(cache_file_path1) or os.path.isfile(cache_file_path2)
 
 
-def make_local_plugins(force=False):
+def make_local_plugins(force: bool = False):
     if not force:
         if _check_local_sync() and _has_local_cache():
             return
@@ -299,7 +299,7 @@ def make_local_plugins(force=False):
         logger.debug(e)
 
 
-def update_local_plugin(name):
+def update_local_plugin(name: str):
     task = ikomia.ik_registry.create_instance(name)
     if task is None:
         return
@@ -340,7 +340,7 @@ def update_local_plugin(name):
             logger.debug(e)
 
 
-def make_online_plugins(force=False):
+def make_online_plugins(force: bool = False):
     if not force and _has_online_cache() and _check_online_sync():
         return
 
@@ -394,6 +394,6 @@ def make_online_plugins(force=False):
         logger.debug(e)
 
 
-def make(force=False):
+def make(force: bool = False):
     make_local_plugins(force)
     make_online_plugins(force)
diff --git a/ikomia/utils/data.py b/ikomia/utils/data.py
index e06388b4..2110984b 100644
--- a/ikomia/utils/data.py
+++ b/ikomia/utils/data.py
@@ -21,7 +21,7 @@
 
 
 class NumpyImage:
-    def __init__(self, path, key):
+    def __init__(self, path: str, key: str):
         self.path = path
         self.data = None
         filename, extension = os.path.splitext(path)
@@ -43,7 +43,7 @@ def get_data(self, dtype=None):
         else:
             return self.data
 
-    def get_2d_data(self, index, dtype=None):
+    def get_2d_data(self, index: int, dtype=None):
         dim_count = len(self.data.shape)
         data = None
 
diff --git a/ikomia/utils/displayIO.py b/ikomia/utils/displayIO.py
index 144acdc2..f71c630a 100644
--- a/ikomia/utils/displayIO.py
+++ b/ikomia/utils/displayIO.py
@@ -65,7 +65,7 @@ def _check_backend():
         matplotlib.use(_backend_name)
 
 
-def _to_plot_color(color):
+def _to_plot_color(color: list) -> list:
     plot_color = [val / 255.0 for val in color]
     return plot_color
 
@@ -565,7 +565,7 @@ def _(obj: dataprocess.CBlobMeasureIO, title: str = "", fig=None, **kwargs):
         child = False
         fig, ax = plt.subplots(1, 1)
 
-    measures = obj.get_masures()
+    measures = obj.get_measures()
     row_labels = list(range(len(measures)))
     col_labels = ["Graphics ID", "Label"]
     max_value_length = 9
diff --git a/ikomia/utils/http.py b/ikomia/utils/http.py
index 1d55cda5..6287ac17 100644
--- a/ikomia/utils/http.py
+++ b/ikomia/utils/http.py
@@ -38,7 +38,7 @@ def wrapper(*argc, **kwargs):
     return wrapper
 
 
-def download_file(url, path, ik_session=None):
+def download_file(url: str, path: str, ik_session=None):
     if ik_session is None:
         s = requests.Session()
     else:
diff --git a/ikomia/utils/io.py b/ikomia/utils/io.py
index dd6669da..a4340234 100644
--- a/ikomia/utils/io.py
+++ b/ikomia/utils/io.py
@@ -1,10 +1,10 @@
 from ikomia import dataprocess
-from ikomia.core import IODataType
+from ikomia.core import IODataType, CWorkflowTaskIO
 from ikomia.dnn.datasetio import IkDatasetIO
 from ikomia.dataprocess.datadictIO import DataDictIO
 
 
-def get_io_type_from_string(type_str: str):
+def get_io_type_from_string(type_str: str) -> IODataType:
     """
     Helper function to get :py:class:`~ikomia.core.pycore.IODataType` enum value from string.
 
@@ -74,7 +74,7 @@ def get_io_type_from_string(type_str: str):
         return IODataType.NONE
 
 
-def create_task_io(io_type: IODataType):
+def create_task_io(io_type: IODataType) -> CWorkflowTaskIO:
     """
     Helper function to create task I/O object from the given data type.
 
diff --git a/ikomia/utils/plugintools.py b/ikomia/utils/plugintools.py
index 007bc0b2..fea0c494 100644
--- a/ikomia/utils/plugintools.py
+++ b/ikomia/utils/plugintools.py
@@ -68,7 +68,7 @@
 
 class SingleFileModuleFinder(modulefinder.ModuleFinder):
 
-    def import_hook(self, name, caller, *arg, **kwarg):
+    def import_hook(self, name: str, caller, *arg, **kwarg):
         if caller is not None and caller.__file__ == self.name:
             # Only call the parent at the top level.
             return modulefinder.ModuleFinder.import_hook(self, name, caller, *arg, **kwarg)
@@ -78,7 +78,7 @@ def __call__(self, node):
         self.run_script(self.name)
 
 
-def get_installed_modules():
+def get_installed_modules() -> dict:
     modules = {}
     result = subprocess.run([sys.executable, "-m", "pip", "list", "--format", "json"], capture_output=True, text=True)
     if result.stdout:
@@ -89,7 +89,7 @@ def get_installed_modules():
     return modules
 
 
-def get_plugin_dependencies(plugin_folder):
+def get_plugin_dependencies(plugin_folder: str) -> list:
     good_modules = []
     bad_modules = []
     file_paths = []
@@ -131,7 +131,7 @@ def get_plugin_dependencies(plugin_folder):
     return list(set(good_modules)), list(set(bad_modules))
 
 
-def install_requirements(directory):
+def install_requirements(directory: str):
     req_files = []
     for root, subdirs, files in os.walk(directory, topdown=True):
         for file in files:
@@ -148,7 +148,7 @@ def install_requirements(directory):
             logger.warning(e)
 
 
-def install_package(name, version):
+def install_package(name: str, version: str):
     if name:
         try:
             subprocess.run([sys.executable, "-m", "pip", "install", f'{name}=={version}'], check=True)
@@ -156,7 +156,7 @@ def install_package(name, version):
             logger.warning(e)
 
 
-def uninstall_package(name):
+def uninstall_package(name: str):
     if name:
         try:
             subprocess.run([sys.executable, "-m", "pip", "uninstall", "-y", name], check=True)
@@ -164,7 +164,7 @@ def uninstall_package(name):
             logger.warning(e)
 
 
-def import_plugin_module(directory, name):
+def import_plugin_module(directory: str, name: str):
     if is_module_imported(name):
         for root, sub_dirs, files in os.walk(directory, topdown=True):
             for file in files:
@@ -175,7 +175,7 @@ def import_plugin_module(directory, name):
     return importlib.import_module(name)
 
 
-def is_module_imported(name):
+def is_module_imported(name: str) -> bool:
     for n, val in globals().items():
         if isinstance(val, types.ModuleType) and name == val.__name__:
             return True
@@ -183,12 +183,13 @@ def is_module_imported(name):
     return False
 
 
-def unload_plugin_module(name):
+def unload_plugin_module(name: str):
     modules_to_delete = [m for m in sys.modules.keys() if name == m]
-    for m in modules_to_delete: del(sys.modules[m])
+    for m in modules_to_delete:
+        del sys.modules[m]
 
 
-def conform_plugin_directory(directory, plugin):
+def conform_plugin_directory(directory: str, plugin: dict) -> str:
     good_dir_name = directory
     base_name = os.path.basename(directory)
 
diff --git a/ikomia/utils/pyqtutils.py b/ikomia/utils/pyqtutils.py
index a3bd1439..d6a782f9 100644
--- a/ikomia/utils/pyqtutils.py
+++ b/ikomia/utils/pyqtutils.py
@@ -20,6 +20,7 @@
 
 from PyQt5.QtWidgets import *
 from PyQt5.QtCore import Qt, QObject, pyqtSignal
+from typing import Optional
 
 
 class BrowseFileWidget(QWidget):
@@ -28,7 +29,8 @@ class BrowseFileWidget(QWidget):
     """
     file_changed = pyqtSignal()
 
-    def __init__(self, path, filter="", tooltip="", mode=QFileDialog.ExistingFile, parent=None):
+    def __init__(self, path: str, filter: str = "", tooltip: str = "", mode=QFileDialog.ExistingFile,
+                 parent: Optional[QObject] = None):
         QWidget.__init__(self, parent)
         self.path = path
         self.tooltip = tooltip
@@ -74,9 +76,9 @@ def show_select_file_dlg(self):
         if file_dialog.exec() == QDialog.Accepted:
             path = file_dialog.selectedFiles()[0]
 
-        return path;
+        return path
 
-    def set_path(self, path):
+    def set_path(self, path: str):
         self.path = path
         self.qedit_file.setText(path)
 
@@ -85,7 +87,7 @@ def clear(self):
         self.qedit_file.clear()
 
 
-def add_edit(grid_layout, row, label, value=""):
+def add_edit(grid_layout, row: int, label: str, value: str = "") -> QLineEdit:
     """
     Add an edit box and its label in the layout at the given row.
     """
@@ -97,7 +99,7 @@ def add_edit(grid_layout, row, label, value=""):
     return qedit
 
 
-def append_edit(grid_layout, label, value=""):
+def append_edit(grid_layout, label: str, value: str = "") -> QLineEdit:
     """
     Append an edit box and its label in the layout.
     """
@@ -105,7 +107,7 @@ def append_edit(grid_layout, label, value=""):
     return add_edit(grid_layout, last_pos, label, value=value)
 
 
-def add_combo(grid_layout, row, label):
+def add_combo(grid_layout, row: int, label: str) -> QComboBox:
     """
     Add a combo box and its label in the layout at the given row.
     """
@@ -116,7 +118,7 @@ def add_combo(grid_layout, row, label):
     return qcombo
 
 
-def append_combo(grid_layout, label):
+def append_combo(grid_layout, label: str) -> QComboBox:
     """
     Append a combo box and its label in the layout.
     """
@@ -124,7 +126,8 @@ def append_combo(grid_layout, label):
     return add_combo(grid_layout, last_pos, label)
 
 
-def add_spin(grid_layout, row, label, value, min=0, max=2147483647, step=1):
+def add_spin(grid_layout, row: int, label: str, value: int,
+             min: int = 0, max: int = 2147483647, step: int = 1) -> QSpinBox:
     """
     Add a composite widget (label + edit box + spin box)
     in the layout at the given row - **integer values**.
@@ -140,7 +143,8 @@ def add_spin(grid_layout, row, label, value, min=0, max=2147483647, step=1):
     return qspin
 
 
-def append_spin(grid_layout, label, value, min=0, max=2147483647, step=1):
+def append_spin(grid_layout, label: str, value: int,
+                min: int = 0, max: int = 2147483647, step: int = 1) -> QSpinBox:
     """
     Append a composite widget (label + edit box + spin box) in the layout -
     **integer values**.
@@ -149,7 +153,9 @@ def append_spin(grid_layout, label, value, min=0, max=2147483647, step=1):
     return add_spin(grid_layout, last_pos, label, value, min, max, step)
 
 
-def add_double_spin(grid_layout, row, label, value, min=0, max=float('inf'), step=1, decimals=4):
+def add_double_spin(grid_layout, row: int, label: str,
+                    value: float, min: float = 0.0, max: float = float('inf'),
+                    step: float = 1, decimals: int = 4) -> QSpinBox:
     """
     Add a composite widget (label + edit box + spin box)
     in the layout at the given row - **float values**.
@@ -166,7 +172,8 @@ def add_double_spin(grid_layout, row, label, value, min=0, max=float('inf'), ste
     return qspin
 
 
-def append_double_spin(grid_layout, label, value, min=0, max=float('inf'), step=1, decimals=4):
+def append_double_spin(grid_layout, label: str, value: float, min: float = 0.0, max: float = float('inf'),
+                       step: float = 1.0, decimals: int = 4) -> QSpinBox:
     """
     Append a composite widget (label + edit box + spin box) in the layout -
     **float values**.
@@ -175,9 +182,9 @@ def append_double_spin(grid_layout, label, value, min=0, max=float('inf'), step=
     return add_double_spin(grid_layout, last_pos, label, value, min, max, step, decimals)
 
 
-def add_check(grid_layout, row, label, checked):
+def add_check(grid_layout, row: int, label: str, checked: bool) -> QCheckBox:
     """
-    Add a check box and its label in the layout at the given row.
+    Add a checkbox and its label in the layout at the given row.
     """
     qcheck = QCheckBox(label)
     qcheck.setChecked(checked)
@@ -185,15 +192,15 @@ def add_check(grid_layout, row, label, checked):
     return qcheck
 
 
-def append_check(grid_layout, label, checked):
+def append_check(grid_layout, label: str, checked: bool) -> QCheckBox:
     """
-    Append a check box and its label in the layout.
+    Append a checkbox and its label in the layout.
     """
     last_pos = grid_layout.rowCount()
     return add_check(grid_layout, last_pos, label, checked)
 
 
-def add_radio(grid_layout, row, label, checked):
+def add_radio(grid_layout, row: int, label: str, checked: bool) -> QRadioButton:
     """
     Add a radio button and its label in the layout at the given row.
     """
@@ -203,7 +210,7 @@ def add_radio(grid_layout, row, label, checked):
     return qradio
 
 
-def append_radio(grid_layout, label, checked):
+def append_radio(grid_layout, label: str, checked: bool) -> QRadioButton:
     """
     Append a radio button and its label in the layout.
     """
@@ -211,7 +218,8 @@ def append_radio(grid_layout, label, checked):
     return add_radio(grid_layout, last_pos, label, checked)
 
 
-def add_slider(grid_layout, row, label, value, min=0, max=2147483647, step=1):
+def add_slider(grid_layout, row: int, label: str, value: int,
+               min: int = 0, max: int = 2147483647, step: int = 1) -> QSlider:
     """
     Add a slider and its label in the layout at the given row.
     """
@@ -226,7 +234,8 @@ def add_slider(grid_layout, row, label, value, min=0, max=2147483647, step=1):
     return qslider
 
 
-def append_slider(grid_layout, label, value, min=0, max=2147483647, step=1):
+def append_slider(grid_layout, label: str, value: int,
+                  min: int = 0, max: int = 2147483647, step: int = 1) -> QSlider:
     """
     Append a slider and its label in the layout.
     """
@@ -234,7 +243,8 @@ def append_slider(grid_layout, label, value, min=0, max=2147483647, step=1):
     return add_slider(grid_layout, last_pos, label, value, min, max, step)
 
 
-def add_browse_file(grid_layout, row=0, label="", path="", filter="", tooltip="", mode=QFileDialog.ExistingFile):
+def add_browse_file(grid_layout, row: int = 0, label: str = "", path: str = "",
+                    filter: str = "", tooltip: str = "", mode=QFileDialog.ExistingFile) -> BrowseFileWidget:
     """
     Add a composite widget (label + edit box + browse button)
     in the layout at the given row - **file or folder**.
@@ -246,7 +256,8 @@ def add_browse_file(grid_layout, row=0, label="", path="", filter="", tooltip=""
     return qbrowse_widget
 
 
-def append_browse_file(grid_layout, label="", path="", filter="", tooltip="", mode=QFileDialog.ExistingFile):
+def append_browse_file(grid_layout, label: str = "", path: str = "",
+                       filter: str = "", tooltip: str = "", mode=QFileDialog.ExistingFile) -> BrowseFileWidget:
     """
     Append a composite widget (label + edit box + browse button) in the layout -
     **file or folder**.


Test patch:
diff --git a/ikomia/utils/tests.py b/ikomia/utils/tests.py
index be47a524..5bd33f72 100644
--- a/ikomia/utils/tests.py
+++ b/ikomia/utils/tests.py
@@ -13,14 +13,15 @@
 # limitations under the License.
 
 import os
-from ikomia.core import task
 import traceback
 import logging
+from ikomia import core
+
 
 logger = logging.getLogger(__name__)
 
 
-def run_for_test(t):
+def run_for_test(t: core.CWorkflowTask) -> tuple:
     logger.info("Running once...")
     try:
         import torch.cuda
@@ -29,6 +30,7 @@ def run_for_test(t):
         logger.info("Cuda torch cache cleaned...")
     except:
         pass
+
     try:
         t.run()
     except Exception as e:
@@ -41,16 +43,16 @@ def run_for_test(t):
         return t.get_parameters(), tb
 
 
-def get_test_image_directory():
+def get_test_image_directory() -> str:
     test_dir = os.path.dirname(os.path.abspath(__file__))
     return os.path.join(test_dir,  "../../tests/data/images")
 
 
-def get_test_video_directory():
+def get_test_video_directory() -> str:
     test_dir = os.path.dirname(os.path.abspath(__file__))
     return os.path.join(test_dir,  "../../tests/data/videos")
 
 
-def get_test_workflow_directory():
+def get_test_workflow_directory() -> str:
     test_dir = os.path.dirname(os.path.abspath(__file__))
     return os.path.join(test_dir,  "../../tests/data/workflows")
diff --git a/tests/test_display.py b/tests/test_display.py
index 439a33d1..9de23800 100644
--- a/tests/test_display.py
+++ b/tests/test_display.py
@@ -108,11 +108,11 @@ def test_display_plot():
     # display plot output
     feature_io = algo.get_output(1)
     displayIO.display(feature_io, label="CalcHist Histogram")
-    feature_io.setPlotType(dataprocess.PlotType.CURVE)
+    feature_io.set_plot_type(dataprocess.PlotType.CURVE)
     displayIO.display(feature_io, label="CalcHist Curves")
-    feature_io.setPlotType(dataprocess.PlotType.MULTIBAR)
+    feature_io.set_plot_type(dataprocess.PlotType.MULTIBAR)
     displayIO.display(feature_io, label="CalcHist Multi-bar")
-    feature_io.setPlotType(dataprocess.PlotType.PIE)
+    feature_io.set_plot_type(dataprocess.PlotType.PIE)
     displayIO.display(feature_io, label="CalcHist Pie chart")
 
 
diff --git a/tests/test_registry.py b/tests/test_registry.py
index 98d6c36c..e7514782 100644
--- a/tests/test_registry.py
+++ b/tests/test_registry.py
@@ -8,7 +8,7 @@
 from ikomia.utils import tests
 
 logger = logging.getLogger(__name__)
-_ikomia_token = os.environ["IKOMIA_TEST_TOKEN"]
+_ikomia_token = os.environ["IKOMIA_TOKEN"]
 
 
 def test_get_local_algorithms():
diff --git a/tests/test_task.py b/tests/test_task.py
index 776af2db..1c96d1a3 100644
--- a/tests/test_task.py
+++ b/tests/test_task.py
@@ -3,6 +3,7 @@
 import argparse
 from ikomia.utils import tests, ik
 from ikomia import core, dataio, dataprocess
+from ikomia.core import task
 import cv2
 import numpy as np
 
@@ -418,7 +419,7 @@ def test_cpp_dnn_train_task():
 
 def test_task_parameters():
     logger.warning("===== Test: set task parameters =====")
-    algo = core.task.create(name=ik.ocv_box_filter().name)
+    algo = task.create(name=ik.ocv_box_filter().name)
     params = algo.get_parameters()
     assert len(params) > 0
 
diff --git a/tests/test_workflow.py b/tests/test_workflow.py
index 60e8a98b..d33091d0 100644
--- a/tests/test_workflow.py
+++ b/tests/test_workflow.py
@@ -356,6 +356,13 @@ def test_set_parameters():
     assert float(params_new[ik.ocv_bilateral_filter.sigmaSpace]) == float(params[ik.ocv_bilateral_filter.sigmaSpace])
     assert float(params_new[ik.ocv_bilateral_filter.sigmaColor]) == float(params[ik.ocv_bilateral_filter.sigmaColor])
 
+    # Pass non string values
+    params = {ik.ocv_bilateral_filter.sigmaSpace: 35.0, ik.ocv_bilateral_filter.sigmaColor: 9.0}
+    wf.set_parameters(params, task_name=ik.ocv_bilateral_filter.name())
+    params_new = bilateral_filter.get_parameters()
+    assert float(params_new[ik.ocv_bilateral_filter.sigmaSpace]) == params[ik.ocv_bilateral_filter.sigmaSpace]
+    assert float(params_new[ik.ocv_bilateral_filter.sigmaColor]) == params[ik.ocv_bilateral_filter.sigmaColor]
+
 
 def test_load():
     logger.warning("===== Test: Workflow::load() =====")