docs: def check_type(obj: Any,
                 candidate_type: Any,
                 reltype: str = 'invariant') -> bool:
      """Tell wether a value correspond to a type,
      optionally specifying the type as contravariant or covariant.
  
      Args:
          obj (Any): The value to check.
          candidate_type (Any): The type to check the object against.
          reltype (:obj:`str`, optional): Variance of the type, can be contravariant,
              covariant or invariant. By default is invariant.
      Returns:
          bool: True if the type is fine, False otherwise
  
      Raises:
          ValueError: When the variance or the type are not among the ones the function can manage.
      """
      if reltype not in ['invariant', 'covariant', 'contravariant']:
          raise ValueError(f' Variadic type {reltype} is unknown')
  
      # builtin type like str, or a class
      if type(candidate_type) == type and reltype in ['invariant']:
          return isinstance(obj, candidate_type)
  
      if type(candidate_type) == type and reltype in ['covariant']:
          return issubclass(obj.__class__, candidate_type)
  
      if type(candidate_type) == type and reltype in ['contravariant']:
          return issubclass(candidate_type, obj.__class__)
  
      # Any accepts everything
      if type(candidate_type) == type(Any):
          return True
  
      # Union, at least one match in __args__
      if type(candidate_type) == type(Union):
          return any(check_type(obj, t, reltype) for t in candidate_type.__args__)
  
      # Tuple, each element matches the corresponding type in __args__
      if type(candidate_type) == type(Tuple) and tuple in candidate_type.__bases__:
          if not hasattr(obj, '__len__'):
              return False
          if len(candidate_type.__args__) != len(obj):
              return False
          return all(check_type(o, t, reltype) for (o, t) in zip(obj, candidate_type.__args__))
  
      # Dict, each (key, value) matches the type in __args__
      if type(candidate_type) == type(Dict) and dict in candidate_type.__bases__:
          if type(obj) != dict:
              return False
          return all(check_type(k, candidate_type.__args__[0], reltype)
                     and check_type(v, candidate_type.__args__[1], reltype)
                     for (k, v) in obj.items())
  
      # List or Set, each element matches the type in __args__
      if type(candidate_type) == type(List) and \
         (list in candidate_type.__bases__ or set in candidate_type.__bases__):
          if not hasattr(obj, '__len__'):
              return False
          return all(check_type(o, candidate_type.__args__[0], reltype) for o in obj)
  
      # TypeVar, this is tricky
      if type(candidate_type) == TypeVar:
          # TODO consider contravariant, variant and bound
          # invariant with a list of constraints, acts like a Tuple
          if not candidate_type.__constraints__:
              return True
          if not (candidate_type.__covariant__ or candidate_type.__contravariant__):
              return any(check_type(obj, t) for t in candidate_type.__constraints__)
  
      if type(candidate_type) == type(Type):
          return check_type(obj, candidate_type.__args__[0], reltype='covariant')
  
      if inspect.isclass(candidate_type) and reltype in ['invariant']:
          return isinstance(obj, candidate_type)
  
      raise ValueError(f'Cannot check against {reltype} type {candidate_type}')