multipliers function which returns a list of functions (more exactly some anonymous functions aka lambdas) :
multipliers()
# the return of multipliers function
[<function multipliers.<locals>.<listcomp>.<lambda> at 0x11243c048>, <function multipliers.<locals>.<listcomp>.<lambda> at 0x11243c0d0>, <function multipliers.<locals>.<listcomp>.<lambda> at 0x11243c158>, <function multipliers.<locals>.<listcomp>.<lambda> at 0x11243c1e0>]
[m(2) for m in multipliers()] list comprehension in which you iterate and call each function from the list returned by multipliers .
The catch is that python closures are late-binding , which means that the values of variables used in closures are looked up at the time the inner function it is called (lambda x : i * x for i in range(4))
That is why for your case the printed list is [6, 6, 6, 6] because the functions from the list (returned by multipliers) are actually looking something like this lambda x : 3 * x and when you are iterating and calling each lambda in your list comprehension [m(2) for m in multipliers()] you are actually calling more or less the same function
If you want to "fix" the behaviour of multipliers function you can do something like this:
def fix_multipliers():
    return [ lambda x, i=i: i*x for i in range(4)]
Afterwards you will see the "expected" behaviours:
print([m(2) for m in fix_multipliers()])
[0, 2, 4, 6]
PS : Closure is a function object that remembers values in enclosing scope even if they are not present in memory aka extend the scope of the inner function.