def binary_search(num_list, num, not_found="none"):
    """Performs a binary search on a sorted list of numbers, returns index.

    Only works properly if the list is sorted, but does not check whether it is
    or not, this is up to the caller.

    Arguments:
        num_list: a sorted list of numbers.
        num: a number to search for.
        not_found: string. Controls what happens if the number is not in the
            list.
            - "none": None is returned.
            - "upper", "force_upper": upper index is returned
            - "lower", "force_lower": lower index is returned
            - "nearest": index to nearest item is returned
            If num is larger than all numbers in num_list,
                if "upper", "lower", "force_lower", or "nearest":
                    index to the last item of the list is returned.
                if "force_upper":
                    index to the next item past the end of the list is returned.
            If num is smaller than all numbers in num_list,
                if "upper", "force_upper", "lower", or "nearest":
                    0 is returned.
                if "force_lower":
                    -1 is returned.
            Default: None.

    returns:
        None if len(num_list) is 0
        None if num is not in num_list and not_found is "none"
        Integer index to item, or perhaps nearest item (depending on
            "not_found" keyword argument).
    """
    if not_found not in (
        "none",
        "upper",
        "force_upper",
        "lower",
        "force_lower",
        "nearest",
    ):
        raise ValueError(
            f"{not_found} is not a recognized value for argument " "'not_found'"
        )
    lower_i, upper_i = 0, len(num_list)
    if upper_i == 0:
        return None
    if num < num_list[0]:
        if not_found == "none":
            return None
        if not_found == "force_lower":
            return -1
        return 0
    if num > num_list[upper_i - 1]:
        if not_found == "none":
            return None
        if not_found == "force_upper":
            return upper_i
        return upper_i - 1

    while True:
        mid_i = (lower_i + upper_i) // 2
        n = num_list[mid_i]
        if n == num:
            return mid_i
        if mid_i == lower_i:
            if not_found == "none":
                return None
            if not_found in ("upper", "force_upper"):
                return upper_i
            if not_found in ("lower", "force_lower"):
                return lower_i
            return lower_i + (num_list[upper_i] - num < num - n)

        if n > num:
            upper_i = mid_i
        else:
            lower_i = mid_i