pure Python Bézier curve implementation
Unnecessary Generator

You've got an unnecessary generator expression here:

def bezier_curve(control_points, number_of_curve_points):
    return [
        bezier_point(control_points, t)
        for t in (
            i / (number_of_curve_points - 1) for i in range(number_of_curve_points)
        )
    ]


You don't need to generate i / (n-1); you could simply pass that argument to the bezier_point() function:

def bezier_curve(control_points, number_of_curve_points):
    return [ bezier_point(control_points, i / (number_of_curve_points - 1))
             for i in range(number_of_curve_points)
           ]


Slight optimization: instead of computing number_of_curve_points - 1 \$O(N)\$ times (pure Python will not cache the result), precompute it:

def bezier_curve(control_points, number_of_curve_points):
    last_point = number_of_curve_points - 1
    return [ bezier_point(control_points, i / last_point )
             for i in range(number_of_curve_points)
           ]


Tail Recursion

Python does not do Tail Call Optimization, so with M control points, you will recursively enter and exit M calls, for each of the N points along your curve.  That is M*N unnecessary stack frame entry/exits.  You should do the looping yourself:

def bezier_point(control_points, t):
    while len(control_points) &gt; 1:
        control_linestring = zip(control_points[:-1], control_points[1:])
        control_points = [(1 - t) * p1 + t * p2 for p1, p2 in control_linestring]
    return control_points[0]


Since we loop while len(control_points) &gt; 1, it should be guaranteed that control_points will only have one point when the loop exits, so return control_points[0] is safe.  The exception is if the function is called with zero control points, but then control_points[0] will properly fail with an IndexError.