This is your first snippet (expanded to two lines and max index reduced from 5 to 3):
lambda_list = [lambda: i for i in range(3)]
results = [x() for x in lambda_list]
It expands to this: i = 0
f0 = lambda: i
i = 1
f1 = lambda: i
i = 2
f2 = lambda: i
results = [f0(), f1(), f2()]
The body of those functions f0, f1, f2, each looks in the function's own dictionary of local variables for "i". But it doesn't exist there, so it looks in the enclosing scope (and keeps doing this recursively up to global scope if necessary). At the next scope it does find a variable "i". This happens at the point you call the function (the same as anything else inside the definition of a function), in this case on that last line when you assign to results. By that point, the "i" variable in the enclosing scope has value 2. So results = [2, 2, 2].[edit: this feature of functions in Python (variables are looked up by name when the function is run) is what lets you define functions foo() and bar() in a module / script that call each other. When the first one is defined, the other one doesn't exist yet, but you're not actually running the code inside the function so it doesn't matter. By the time you come to call it later, the other function does exist in the enclosing (module level) scope so the lookup succeeds.]
This is your second snippet (I changed one of the variable names from i to j for clarity, but you can rename it back and it would work the same way):
lambda_list = [lambda j=i: j for i in range(3)]
results = [x() for x in lambda_list]
it expands like this: i = 0
f0 = lambda j=i: j
i = 1
f1 = lambda j=i: j
i = 2
f2 = lambda j=i: j
results = [f0(), f1(), f2()]
But defaults of function arguments are evaluated at the point the function is declared, not when it's called (as noted elsewhere in this thread - hence the def foo(x=[]) gotcha!). You can imagine the second line of code above is like this, just to really drive the point home [edit: I originally used a made up syntax, but I updated to code that actually works]: f0 = lambda j: j
f0.__defaults__ = i,
f0.__kwdefaults__ = {"j": i}
So, f0 has the specific value 0 baked into it, and similarly f1 and f2 have 1 and 2. Therefore results = [0, 1, 2].