Functional abstraction to find nth root of a number - Newton raphson
First, congratulations for the work you did : one can easily see that you put some thinking into it and as far as I have tested it, it works.

However, I still have a few comments and most of them are about the fact that you might have tried to be too clever.

Indeed, the use of both recursion and nested functions is a bit too much for my brain. Among other things, it took me a while to spot that the guess argument of the newtons_method is not used (and I am not even sure of it, am I right ?). You could make is easier to grasp by using names conveing more meaning.

Here's my attempt to un-nest functions and to change names (I am fully aware that they are not perfect) :

def recur_improve(next_step, isdone, x, i, max_iterations):
    if i &gt; max_iterations or isdone(x):
       return x
    else:
       return recur_improve(next_step, isdone, next_step(x), i + 1, max_iterations)

def improve(next_step, isdone, initval=1, max_iterations=100):
    return recur_improve(next_step, isdone, initval, 1, max_iterations)

def newtons_method(fn, max_iterations=100):
    def newtons_step(x):
        return x - fn(x) / approx_deriv(fn, x)
    def newtons_isdone(x):
        ALLOWED_ERROR_MARGIN = 0.0000001
        return abs(fn(x)) &lt;= ALLOWED_ERROR_MARGIN
    return improve(newtons_step,
                        newtons_isdone,
                        max_iterations)


Then, the recursion can be easily replaced by a while loop :

def improve(next_step, isdone, initval=1, max_iterations=100):
    x = initval
    i = 1
    while i &lt; max_iterations and not isdone(x):
        x = next_step(x)
        i +=1
    return x


Then, one can see that counting iterations is boring. We have builtins to do this in a concise and efficient way.

def improve(next_step, isdone, initval=1, max_iterations=100):
    x = initval
    i = 1
    for i in range(max_iterations):
        if isdone(x):
            break
        x = next_step(x)
    return x


(I might have introduced on off-by-one error in the iteration counts but I can't be bothered).

Then, instead of passing functions using your original functions, you could just write conditions directly :

def improve(fn, initval=1, max_iterations=100, epsilon=0.0000001):
    x = initval
    for i in range(max_iterations):
        if abs(fn(x)) &lt;= epsilon:
            break
        x -= fn(x) / approx_deriv(fn, x)
    return x


Then you don't even need to have multiple functions anymore and your code boils down to :

def newtons_method(fn, initval, max_iterations=100, epsilon=0.00000001):
    x = initval
    for i in range(max_iterations):
        if abs(fn(x)) &lt;= epsilon:
            break
        x -= fn(x) / approx_deriv(fn, x)
    return x


It seems much clearer to me.

Also, your def nth_root(rootfunc, x) function doesn't seem really useful to me. You could just have :

def cuberoot(x):
    return newtons_method(lambda y: y * y * y - x, x)

result = cuberoot(27**3)

print(result)


And get the result you expect.

Finally, your function deserves some documentation.

Final code :

def approx_deriv(fn, x, dx=0.00001):
    """ some docstring. """
    return (fn(x + dx) - fn(x))/dx


def newtons_method(fn, initval, max_iterations=100, epsilon=0.00000001):
    """ some docstring. """
    x = initval
    for i in range(max_iterations):
        if abs(fn(x)) &lt;= epsilon:
            break
        x -= fn(x) / approx_deriv(fn, x)
    return x

def cuberoot(x):
    """ some docstring. """
    return newtons_method(lambda y: y * y * y - x, x)

result = cuberoot(27**3)

print(result)