Start with something quick. You've been working with (name, sku, price, stock) tuples all week. The warehouse catalog has products sorted by name right now. Diane wants them sorted by stock level — lowest first so she can prioritize restocking. Without looking anything up, how would you approach that?
sorted(products) would sort by the first element — name, alphabetically. That's not what I want. I need to sort by index 3. I'd use a lambda — sorted(products, key=lambda p: p[3]).
Right. And before I show you the del statement that's technically the day's topic, I want to take a detour through sorting because the == versus is distinction is hiding inside it. What does Python compare when it sorts tuples with no key function?
Without a key... it would compare element by element? First element first, then the second if the first tied, and so on?
Exactly. That's called lexicographic comparison — the same logic dictionaries use for words. ("Bolt", "SKU-002", 4.99, 300) versus ("Widget", "SKU-001", 24.99, 150) — Python compares the first element, "Bolt" < "Widget", and stops there. If the first elements tied, it would move to the second.
So key=lambda p: p[3] tells Python to only compare on stock level and ignore the rest. That's why key= exists — to tell Python what to compare, not how to sort the whole tuple.
That's exactly right, and most developers don't articulate it that clearly until they get burned by an unexpected sort order. Now — == versus is. What do you think the difference is?
== checks if two values are equal. is... I've used it before but I'm not confident about what it actually checks.
== checks value equality — the contents match. is checks identity — it's literally the same object in memory. Two separate objects can be equal without being identical. Think of two laminated shelf labels that both say "Widget-A | SKU-1001 | $24.99". They're == — identical content. But they're not is — one is bolted to Rack 3 and one to Rack 7. Separate physical objects.
label_a = ("Widget-A", "SKU-1001", 24.99)
label_b = ("Widget-A", "SKU-1001", 24.99)
print(label_a == label_b) # True — same content
print(label_a is label_b) # False — different objectsI've been using is for things I should have been using == for. Like checking if a product matched a template — that was comparing values, not identity.
The correct rule: use is for None, True, and False. Python guarantees there is only ever one None object in memory, so x is None is both correct and slightly more efficient than x == None. For everything else — data values, tuples, strings, numbers — use ==.
# Correct for None checks
if result is None:
return []
# Works but triggers style warnings
if result == None:
return []Okay. is for None/True/False. == for data. That rule I can keep.
Now the day's actual syntax. del. You've been using .remove() to take items out of lists — by value. del works by index:
products = [
("Widget-A", "SKU-1001", 24.99, 150),
("Bolt-M6", "SKU-2042", 4.99, 8),
("Hard-Hat", "SKU-3300", 19.99, 45),
]
del products[0] # removes Widget-A entirely — in-place, no return value
print(len(products)) # 2So del mutates the list in-place and gives you nothing back. .pop() also removes by index but returns the removed item. .remove() removes by value. Three operations, three different needs.
That's the full picture. When you know the index and don't need the value: del. When you want the removed item back: .pop(). When you want to remove by value: .remove(). And del has one more use that's less obvious — you can delete a variable binding entirely:
temp_batch = ["SKU-9001", "SKU-9002"] # processing variable
del temp_batch
print(temp_batch) # NameError: name 'temp_batch' is not definedWhy would you delete a variable name? I've never seen that in the wild.
Two situations. First: long functions that process batches — del signals that a value has served its purpose and makes any accidental reuse fail loudly rather than silently. Second: signaling intent in code review. If you see del temp_batch at the bottom of a processing block, you know the developer considered the lifecycle and made an explicit decision.
In the Monday report I accidentally reused a loop variable after the loop ended and got stale data. A del after the loop would have turned that into an immediate NameError instead of a wrong but silent result.
Future-Maya is much less confused with del in her toolbelt. Now — write the sort function. sort_by_stock(products, reverse=False). Use sorted() with a key, not .sort(). Critical distinction:
# sorted() — returns NEW list, doesn't touch the original
new_list = sorted(products, key=lambda p: p[3])
# .sort() — modifies original IN-PLACE, returns None
result = products.sort(key=lambda p: p[3]) # result is None!.sort() returns None? I've definitely assigned that to a variable before and then wondered why my list disappeared.
You are not the first. You will not be the last. The trap has claimed every Python developer I know at least once. sorted() is safe — it always returns the new list. .sort() is dangerous for this exact reason.
Got it. sorted() for the function, pass reverse through directly. One line.
One more forward hook before you write it. Tomorrow you're going to stop using product[3] entirely. Named tuples give you product.stock — the field has a name, not a number. Everything you've built this week with plain tuples translates directly. The only change is readability, and readability is everything six months later when you're debugging.
I've been wondering when that was coming. product.stock is so much cleaner than product[3].
Tomorrow.
The del statement removes a name binding or a sequence element. Unlike methods that return something, del is purely a statement — it produces no value.
del my_list[i] removes element at index i in-place; the list shrinks.del my_var removes the variable name from the current scope; any subsequent access raises NameError.del my_dict[key] removes a key-value pair from a dict in-place.sorted() vs .sort(): sorted(iterable, key=..., reverse=...) always returns a new list and never modifies its input. list.sort(key=..., reverse=...) sorts in-place and returns None. Assigning the result of .sort() is one of the most common Python bugs.
== vs is: == tests value equality (calls __eq__). is tests identity — whether two names point at the exact same object in memory. Use is only for None, True, and False.
# Safe sort — returns new list
reorder_list = sorted(products, key=lambda p: p[3]) # stock at index 3
# Common bug — result is None
result = products.sort(key=lambda p: p[3]) # products sorted, result is None
# Correct None check
if found_product is None:
return "Not found"Pitfall 1: Assigning .sort()'s return value. result = my_list.sort() gives result = None. Use sorted() if you need the sorted list assigned to a variable.
Pitfall 2: Using is to compare data values. Small integers and short strings may share objects due to Python's interning, making x is 1 sometimes True — but this is an implementation detail, not a guarantee. Always use == for data comparisons.
Pitfall 3: Lexicographic tuple sort surprises. sorted(tuples) with no key sorts element by element. A product tuple ("Bolt", ..., ..., 300) sorts before ("Widget", ..., ..., 8) because "Bolt" < "Widget" — regardless of stock level. Always specify key= when you mean to sort on a specific field.
The key= parameter accepts any callable that takes one argument and returns a sortable value. operator.itemgetter(3) is a faster alternative to lambda p: p[3] for large lists — it skips Python function call overhead. For multi-key sorts, itemgetter(3, 0) sorts by stock first, then by name on ties. Python's sort is stable, meaning equal elements preserve their original relative order — important when sorting a pre-sorted list by a secondary key.
Sign up to write and run code in this lesson.
Start with something quick. You've been working with (name, sku, price, stock) tuples all week. The warehouse catalog has products sorted by name right now. Diane wants them sorted by stock level — lowest first so she can prioritize restocking. Without looking anything up, how would you approach that?
sorted(products) would sort by the first element — name, alphabetically. That's not what I want. I need to sort by index 3. I'd use a lambda — sorted(products, key=lambda p: p[3]).
Right. And before I show you the del statement that's technically the day's topic, I want to take a detour through sorting because the == versus is distinction is hiding inside it. What does Python compare when it sorts tuples with no key function?
Without a key... it would compare element by element? First element first, then the second if the first tied, and so on?
Exactly. That's called lexicographic comparison — the same logic dictionaries use for words. ("Bolt", "SKU-002", 4.99, 300) versus ("Widget", "SKU-001", 24.99, 150) — Python compares the first element, "Bolt" < "Widget", and stops there. If the first elements tied, it would move to the second.
So key=lambda p: p[3] tells Python to only compare on stock level and ignore the rest. That's why key= exists — to tell Python what to compare, not how to sort the whole tuple.
That's exactly right, and most developers don't articulate it that clearly until they get burned by an unexpected sort order. Now — == versus is. What do you think the difference is?
== checks if two values are equal. is... I've used it before but I'm not confident about what it actually checks.
== checks value equality — the contents match. is checks identity — it's literally the same object in memory. Two separate objects can be equal without being identical. Think of two laminated shelf labels that both say "Widget-A | SKU-1001 | $24.99". They're == — identical content. But they're not is — one is bolted to Rack 3 and one to Rack 7. Separate physical objects.
label_a = ("Widget-A", "SKU-1001", 24.99)
label_b = ("Widget-A", "SKU-1001", 24.99)
print(label_a == label_b) # True — same content
print(label_a is label_b) # False — different objectsI've been using is for things I should have been using == for. Like checking if a product matched a template — that was comparing values, not identity.
The correct rule: use is for None, True, and False. Python guarantees there is only ever one None object in memory, so x is None is both correct and slightly more efficient than x == None. For everything else — data values, tuples, strings, numbers — use ==.
# Correct for None checks
if result is None:
return []
# Works but triggers style warnings
if result == None:
return []Okay. is for None/True/False. == for data. That rule I can keep.
Now the day's actual syntax. del. You've been using .remove() to take items out of lists — by value. del works by index:
products = [
("Widget-A", "SKU-1001", 24.99, 150),
("Bolt-M6", "SKU-2042", 4.99, 8),
("Hard-Hat", "SKU-3300", 19.99, 45),
]
del products[0] # removes Widget-A entirely — in-place, no return value
print(len(products)) # 2So del mutates the list in-place and gives you nothing back. .pop() also removes by index but returns the removed item. .remove() removes by value. Three operations, three different needs.
That's the full picture. When you know the index and don't need the value: del. When you want the removed item back: .pop(). When you want to remove by value: .remove(). And del has one more use that's less obvious — you can delete a variable binding entirely:
temp_batch = ["SKU-9001", "SKU-9002"] # processing variable
del temp_batch
print(temp_batch) # NameError: name 'temp_batch' is not definedWhy would you delete a variable name? I've never seen that in the wild.
Two situations. First: long functions that process batches — del signals that a value has served its purpose and makes any accidental reuse fail loudly rather than silently. Second: signaling intent in code review. If you see del temp_batch at the bottom of a processing block, you know the developer considered the lifecycle and made an explicit decision.
In the Monday report I accidentally reused a loop variable after the loop ended and got stale data. A del after the loop would have turned that into an immediate NameError instead of a wrong but silent result.
Future-Maya is much less confused with del in her toolbelt. Now — write the sort function. sort_by_stock(products, reverse=False). Use sorted() with a key, not .sort(). Critical distinction:
# sorted() — returns NEW list, doesn't touch the original
new_list = sorted(products, key=lambda p: p[3])
# .sort() — modifies original IN-PLACE, returns None
result = products.sort(key=lambda p: p[3]) # result is None!.sort() returns None? I've definitely assigned that to a variable before and then wondered why my list disappeared.
You are not the first. You will not be the last. The trap has claimed every Python developer I know at least once. sorted() is safe — it always returns the new list. .sort() is dangerous for this exact reason.
Got it. sorted() for the function, pass reverse through directly. One line.
One more forward hook before you write it. Tomorrow you're going to stop using product[3] entirely. Named tuples give you product.stock — the field has a name, not a number. Everything you've built this week with plain tuples translates directly. The only change is readability, and readability is everything six months later when you're debugging.
I've been wondering when that was coming. product.stock is so much cleaner than product[3].
Tomorrow.
The del statement removes a name binding or a sequence element. Unlike methods that return something, del is purely a statement — it produces no value.
del my_list[i] removes element at index i in-place; the list shrinks.del my_var removes the variable name from the current scope; any subsequent access raises NameError.del my_dict[key] removes a key-value pair from a dict in-place.sorted() vs .sort(): sorted(iterable, key=..., reverse=...) always returns a new list and never modifies its input. list.sort(key=..., reverse=...) sorts in-place and returns None. Assigning the result of .sort() is one of the most common Python bugs.
== vs is: == tests value equality (calls __eq__). is tests identity — whether two names point at the exact same object in memory. Use is only for None, True, and False.
# Safe sort — returns new list
reorder_list = sorted(products, key=lambda p: p[3]) # stock at index 3
# Common bug — result is None
result = products.sort(key=lambda p: p[3]) # products sorted, result is None
# Correct None check
if found_product is None:
return "Not found"Pitfall 1: Assigning .sort()'s return value. result = my_list.sort() gives result = None. Use sorted() if you need the sorted list assigned to a variable.
Pitfall 2: Using is to compare data values. Small integers and short strings may share objects due to Python's interning, making x is 1 sometimes True — but this is an implementation detail, not a guarantee. Always use == for data comparisons.
Pitfall 3: Lexicographic tuple sort surprises. sorted(tuples) with no key sorts element by element. A product tuple ("Bolt", ..., ..., 300) sorts before ("Widget", ..., ..., 8) because "Bolt" < "Widget" — regardless of stock level. Always specify key= when you mean to sort on a specific field.
The key= parameter accepts any callable that takes one argument and returns a sortable value. operator.itemgetter(3) is a faster alternative to lambda p: p[3] for large lists — it skips Python function call overhead. For multi-key sorts, itemgetter(3, 0) sorts by stock first, then by name on ties. Python's sort is stable, meaning equal elements preserve their original relative order — important when sorting a pre-sorted list by a secondary key.