def bisection(f, a, b, TOL, N):

    """To find a solution to f(x) = 0

    Finds the solution to f(x) = 0 given the continuous function f on the interval [a, b], where
    f(a) and f(b) have opposite signs.

    Args:
        f: a function to be evaluated (can be a lambda or def).
        a: the lower bound in the given range
        b: the upper bound in the given range
        TOL: The tolerance (How big an error is acceptable)
        N: The number of iterations to be perfomed

    Returns: Has two results. The first one is the solution after N iterations of f(x) = 0.
        The second one is a lists of lists for every solution after n iterations.
        For example:
        This would be the result after 3 iterations
            [[1, 1.5, 1.5, 2.375], [1.25, 1.5, 1.25, -1.796875], [1.25, 1.375, 1.375, 0.162109375]]
        if the function f was
            f = lambda x: x ** 3 + 4 * x ** 2 - 10
        with a = 1, b = 2, TOL = 0.00001, N = 13
    Raises:
        None
    """
    p = a
    result = []
    # checking if the solution lies in the given range
    if f(a) * f(b) >= 0:
        print("f(a) and f(b) need to have opposite signs\n")
        return

    i = 1
    FA = f(a)
    while i <= N:
        p = a + (b - a) / 2  # (Compute pi.)
        FP = f(p)
        if FP == 0.0 or (b - a) / 2 < TOL:
            # (Procedure completed successfully.)
            break
        i = i + 1
        if FA * FP > 0:
            a = p  # (Compute ai, bi.)
            FA = FP
        else:
            b = p  # (FA is unchanged.)
        result.append([a, b, p, f(p)])

    if i == N:
        print(f"Method failed after {N} iterations")
        # (The procedure was unsuccessful.)
    return p, result