def check_arg(name, value, exp_type, exp_range=()):
    """
    Checks a value against an expected type and range.
    Used for data preprocessign and projections.

    Keyword Arguments:
    - name: name of the argument, for error messages
    - value: value
    - exp_type: the type that the value should have
    - exp_range: in case of number a tuple or list with (min, max)
                 in case of strings a tuple or list with all options
                 if empty (default), all values of the correct type will be allowed
    Returns:
    - Value if valid
    Raises:
    - Exception if invalid
    """
    # check if None
    if value == None:
        raise Exception('Missing argument "{}"'.format(name))

    # check type
    if not isinstance(value, exp_type):
        # accept ints for floats
        if not (exp_type is float and type(value) is int):
            raise Exception('Invalid argument "{}" with value "{}", should be {} but is "{}"'.format(
                name, value, exp_type, type(value)))

    # check range for strings and numbers
    if exp_type == str and len(exp_range) != 0 and value not in exp_range:
        raise Exception(
            'Invalid argument "{}", value must be in {}'.format(name, exp_range))
    elif exp_type == int or exp_type == float:
        if len(exp_range) != 0 and not exp_range[0] <= value <= exp_range[1]:
            raise Exception(
                'Invalid argument "{}", value must be in {}'.format(name, exp_range))
    return value