Python loop exercise help

Hi, I am new to coding.
Why does my code still slice the list when it gets to 11, despite it being an odd number?
The function should be removing any even number from the front of the list
Intended output should be [11, 12, 15]

def delete_starting_evens(my_list):
 while len(my_list) > 0:
  for num in my_list:
    if num % 2 == 0:
      my_list = my_list[1:]
    else:
      my_list[0:]
  return my_list

print(delete_starting_evens([4, 8, 10, 11, 12, 15]))

Output:
[12, 15]

The code modifies a mutable container, whilst iterating over it. Don’t do that. This is bad practise, and inevitably difficult to reason about, and can even be tricky to implement a simple goal with.

In general, to modify a container in place, making an iteration of an index over a range is usually more predictable. If memory is not a concern, list comprehensions are great. It’s better to use native list methods like .remove than making slices from a memory point of view too (but tracking indices and items is a little trickier then).

In this situation in particular, consider the converse, what you want to return, not what you want to delete: a simple search for the first odd number and a slice from there onwards is all you need.

Also that while loop adds nothing over an if statement and another return, and is asking for trouble.

Anyway I think the problem could be because when you start the for loop, iter is called on my_list, making an iterator over the original list items. This iterator is not updated when you subsequently reassign the name my_list to subslices of the new list it currently points to. So the for loop always iterates over all the numbers, and chops off the first number in my_list whatever that is, whenever an even number is found in the original list. There are 6 numbers, 4 of which are even, so the 1st 4 elements are chopped off one at a time, leaving the last 2.

But as I said, it’s difficult to reason about (notoriously so) so I could be wrong. My idea could be tested by assigning my_list to anything else, e.g. a string or an empty tuple, and seeing if the inner loop still iterates over each element of the original list.

1 Like

You’re absolutely right that it’s hard to reason about :slight_smile: This isn’t exactly what happens, but that only strengthens your point that mutating the thing you’re iterating over makes it hard to figure out what’s happening.

Yes, and importantly, doing this with a single slice is not just faster, it’s much much easier to think about.

Thanks :). I’m sure there’re subtleties I’ve overlooked, but I can’t figure out what they are:

def delete_starting_evens(my_list):
  for num in my_list:
    print(f'{num=} {my_list=}')
    if num % 2 == 0:
      my_list = my_list[1:]    
  return my_list

print(delete_starting_evens([4, 8, 10, 11, 12, 15]))
num=4 my_list=[4, 8, 10, 11, 12, 15]
num=8 my_list=[8, 10, 11, 12, 15]
num=10 my_list=[10, 11, 12, 15]
num=11 my_list=[11, 12, 15]
num=12 my_list=[11, 12, 15]
num=15 my_list=[12, 15]
[12, 15]

This is an example of a different way to mess up loops like this. It could be called the “pointer issue” (or “repeated off-by-one issue”). It naively mutates a container destructively, but the iterator does not account for the elements removed from it as OP would perhaps intend (it’s possible to manually adjust an index, by tracking an offset and adding that to it, but that rapidly leads to messy code):

def delete_starting_evens(my_list):
  for num in my_list:
    print(f'{num=} {my_list=}')
    if num % 2 == 0:
      my_list.remove(num)
    else:
      break
  return my_list
num=4 my_list=[4, 8, 10, 11, 12, 15]
num=10 my_list=[8, 10, 11, 12, 15]
num=12 my_list=[8, 11, 12, 15]
[8, 11, 15]

I would do it like that: instead of iterating over a list and modifying it on the fly (which, as James pointed out, is a really, really bad thing) I would first found where is the last even number from the start of a list and then chop off a whole block of the list:

def delete_starting_evens(my_list):
   last = -1
   for i in range(len(my_list)):
      if my_list[i] % 2 != 0:
           last = i
           break
   if last > 0:
      #chop!
      del my_list[:last]
   return my_list

now it works:
[11, 12, 15]

1 Like

You sure? This sounds exactly right to me, what difference are you seeing? (ofcourse, the fact that we are still disagreeing proofs the point even more xD)

I am so new to coding half the stuff you’ve written has flown over my head. But I think I kind of get it. Instead of making a function modify my initial argument, it’s better to try and make the function take out parts of the argument and formulate an output that way?

Actually, looks like I was misreading part of the OP’s code, and there’s a line in it that does absolutely nothing. So you WERE right. In any case, the solution was the same: don’t iterate and mutate.

2 Likes

Generally the advice is to try and iterate over fixed predictable things.

But If possible, yes, I would also prefer returning new copies to modifying in place, especially via a function with side effects. That leads to much cleaner, functional style code, and testable functions.

If if it also needed to work on lists that take up more than half of the available memory, it can’t be copied in memory, so the list should be mutated in place. In resource constrained environments though, is Python really the best choice in the first place?

Bearing in mind that two lists referring to the same items aren’t duplicating those items, it would have to be a pretty insanely big list for that to be a concern.

That’s not true. And the end of your very same post shows that you know it. Confusing.

Indeed. That’s what the code attempts, not what it does. And is more a general description of what not do do, in order to avoid associated bugs.

[

Very often defining the problem will give very strong hint to the solution. This problem can be defined: “take slice from given list starting from first non-even number till end”. The solution to the problem lies in finding first non-even number and its index for making slice. Python has built in function enumerate which can be used. This is apparently homework therefore just an idea:

>>> sample = [4, 8, 10, 11, 12, 15]
>>> for i, num in enumerate(sample):
...    if num % 2 != 0:
...       print(sample[i:])
...       break
...
[11, 12, 15]