.remove makes a difference because you are updating the iterator as it interates.
Imagine i as a pointer which points to the 0th index of a and goes till the end of length(a).(Here, length(a) is itself changing).
So your code is like:
a = [1, 2, 3, 4]
for i in a:
    a.remove(i)
    print(i)
So internally , i is initialised to point to the 0th index of a in the first iteration.
In the next iteration, i is automatically initialised to point to the  1st index of a (if it exists)
In the next iteration, i is is automatically initialised to point to the 2nd index of a (if it exists) and so on...
So here is what happens in the loop:
Iteration 1
a = [1, 2, 3, 4]
i -> 0th index of a = 1
a.remove(i)
a -> [2, 3, 4]
print(i) -> prints 1
Iteration  2
a = [2, 3, 4]
i -> 1st index of a = 3
a.remove(i)
a -> [2, 4]
print(i) -> prints 3
Iteration 3
a = [2, 4]
i -> 2nd index of a = Does not exist -> Quit iteration 
Because of this, if you run the above program, only 1 and 3 will be printed.