Should you not think of/treat querysets as python lists?

Really basic question here but I was under the assumption that when we call model_name.objects.all() the returned queryset can be thought of as a list since its iteratable you can slice throught it using python slice syntax. One of the first mistakes most new programmers make in any language is iterating over a list while deleting its elements, which of course does not produce the expected output. Eg:

ls = [i for i in range(10)]
for i in ls:
    ls.remove(i)
print(ls)

will produce [1, 3, 5, 7, 9].

I assumed this would be the case with querysets as well but upon trying something like this:

for obj in my_model.objects.all():
    if some_condition:
        my_model.delete(obj)

I was surprised to find out it did in fact delete all the elements correctly. And if I removed the condition it deletes them all. I was expecting I’d have to do something like adding them to another list of objs_to_delete and then deleting them afterwards.
My question is: 1) Is it wrong to think of querysets as practically synonymous with lists then?
2) Although this seems possible, would it be bad practise to iterate over objects.all() and delete them since it reinforces a bad programming habbit when applied to normal lists?

You actually raise a couple of different issues, each somewhat subtle.

First, with your examples, you have an “Apples / Oranges” situation:

Notice that in this case, you’re executing the remove method on the ls object. ls is the list over which you are iterating.

In this sample, you are not iterating over my_model - you are iterating over the result set returned by the all method of the objects manager associated with the my_model class.

The proper parallel would be more something like this:

my_model_list = list(MyModel.objects.all())
for obj in my_model_list:
    if some_condition:
        my_model_list.remove(obj)
print(my_model_list)

… which is something completely different.

Now, as to your specific questions:

Technically, yes, it is wrong - but not for anything associated with the previous example. A QuerySet can exist either “evaluated” or “not-evaluated”. When not evaluated, a QuerySet is best thought of as an SQL query that hasn’t been executed yet. That means that you can continue to apply filters and other transformations to that QuerySet - and still not have actually accessed the database at that point. (For information about when a QuerySet gets evaluated, see When QuerySets are evaluated.)

Once a QuerySet has been evaluated, then you are generally safe in thinking about a QuerySet as an iterable. (I’d be more inclined to think of it as a tuple rather than a list, but that’s just me.)

It’s a bad habit, not because of it reinforcing a bad style when applied to normal lists, but because it’s extremely sub-optimal.
As a general rule, you want to minimize the number of actual physical queries your processes need to perform. In just about every case where you want to do a bulk delete, you want to build your queryset to identify those elements to be deleted, then call the delete method on the queryset

Ken

Thanks Ken, that was a superb explanation. I’d read about evaluated vs non evaluated in the docs but the details were still a little fuzzy for me, but reading about it again now after working with Django more it makes a lot more sense. Thank you also for the tip regarding deleting all the objects in the queryset rather than iterating over it and deleting them individually.

Mike