I suppose you could roughly translate the inner loop in that above expression to:
for e in x:
ee = iter(e)
try:
e = next(ee)
while True
print e
e = next(ee)
except StopIteration
pass
Note that the key thing here is in the statement: for e in ..., ... is converted to an iterator via the iterator protocol. The object you actually iterate over is a separate object from the e you gave it initially. Since it's a separate object (stored separately from its name in the current scope to allow it to be iterated over) there is no problem with binding a new variable to that name in the current scope -- Maybe I should say that there is no problem other than it makes the code really hard to follow.
It's effectively the same reason you don't have a problem doing this:
A = [['foo']] #Define A
b = A[0] #Take information from A and rebind it to something else
c = A #We can even take the entire reference and bind/alias it to a new name.
A = 'bar' #Re-assign A -- Python doesn't care that A already existed.
Here are a couple more things to think about:
x = [1,2,3,4]
for a in x:
print a
next(a) #Raises an error because lists aren't iterators!
Now a seldom used, (but sometimes necessary) idiom:
x = [1,2,3,4]
y = iter(x) #create an iterator from the list x
for a in y:
print a
#This next line is OK.
#We also consume the next value in the loop since `iter(y)` returns `y`!
#In fact, This is the easiest way to get a handle on the object you're
#actually iterating over.
next(y)
finally:
x = [1,2,3,4]
y = iter(x) #create an iterator from the list x
for a in y:
print a
#This effectively does nothing to your loop because you're rebinding
#a local variable -- You're not actually changing the iterator you're
#iterating over, just as `A = 'bar'` doesn't change the value of
#the variable `c` in one of the previous examples.
y = iter(range(10))