Slicing, Mutable versus Immutable, and is versus ==
- Cut lists and strings with Slicing, including a negative step to reverse
- Classify types as Mutable or Immutable and explain the difference
- Understand what happens when two variables point to the same object, and how to create a copy
- Tell apart
==(the same value) andis(the same object)
What will we learn?
- Slicing — how to cut out part of a list, a tuple or a string.
- Mutable versus Immutable — what can be changed and what cannot, and why it matters.
- References — what happens when you write
b = a. isversus==— “the same value” versus “exactly the same object”.
Why do we need this?
- Slicing is everywhere: the first 10 results, the last 3 characters of a card number (
****1234), “page 2” of a list. - Mutable is the source of one of the most confusing bugs in Python: “I changed one list, and suddenly the second one changed too!”. Understanding this topic prevents hours of debugging. And in the OOP topic, objects of classes behave exactly like this.
Explanation from the basics
Slicing
Slicing creates a new sequence from part of an existing sequence:
sequence[start:stop:step]- start — where to begin (included). Default: from the start.
- stop — where to stop (not included!, like in
range). Default: until the end. - step — the jump. Default: 1. A negative step = from the end to the start.
letters = ["a", "b", "c", "d", "e", "f"]
index: 0 1 2 3 4 5
negative: -6 -5 -4 -3 -2 -1Slicing does not throw an IndexError even if you go out of range. It simply returns what there is.
Mutable versus Immutable
| Immutable (cannot be changed) | Mutable (can be changed) |
|---|---|
int, float, bool, str, tuple | list, dict, set |
Immutable = you cannot change the object itself. Every “change” creates a new object:
name = "dana"
name.upper() # returns a NEW string "DANA"; name is still "dana"
name = name.upper() # now name points to the new stringMutable = you can change the object in place (append, d[k] = v), and it stays the same object.
A variable = a Reference
Remember the sticker analogy? A variable is a sticker with a name, stuck onto an object. It does not “contain” the object, it points to it.
a = [1, 2, 3]
b = a # NOT a copy! b is a second sticker on the SAME lista ──┐
├──► [1, 2, 3] one list in memory, two names
b ──┘If the list is Mutable and you change it through b, the change is also visible through a. It is the same object.
== versus is
| Operator | Asks | Example |
|---|---|---|
== | Are the values equal? | [1, 2] == [1, 2] → True |
is | Is it exactly the same object in memory? | [1, 2] is [1, 2] → False (two separate lists) |
id(x) returns a unique identifier of an object. a is b is equivalent to id(a) == id(b).
Rule: compare values with ==. is is used almost only for comparing with None: if result is None:.
Two identical twins: == → True (they look the same). is → False (two different people). On the other hand, “Dana” and “Ruti’s daughter” are the same person (is → True): two names for the same one. If Dana gets a haircut, “Ruti’s daughter” has had a haircut too.
Syntax
# slicing
items[2:5] # indexes 2, 3, 4
items[:3] # first 3
items[-3:] # last 3
items[::2] # every second item
items[::-1] # reversed copy
items[:] # a full (shallow) copy
# copies of mutable objects
copy1 = items[:]
copy2 = items.copy()
copy3 = list(items)
d_copy = my_dict.copy()
# identity vs equality
a == b # same value?
a is b # same object?
x is None # the correct way to check for None
id(a) # the object's unique identity numberFirst code example
week-2/slicing_demo.py:
letters = ["a", "b", "c", "d", "e", "f"]
print(letters[1:4])
print(letters[:2])
print(letters[-2:])
print(letters[::2])
print(letters[::-1])
print(letters[2:100])
card_number = "4580123498761234"
print("Card ending in", card_number[-4:])
print("Masked:", "*" * 12 + card_number[-4:])Output:
['b', 'c', 'd']
['a', 'b']
['e', 'f']
['a', 'c', 'e']
['f', 'e', 'd', 'c', 'b', 'a']
['c', 'd', 'e', 'f']
Card ending in 1234
Masked: ************1234Breaking down the code line by line
| Expression | Indexes | Explanation |
|---|---|---|
[1:4] | 1, 2, 3 | 4 is not included |
[:2] | 0, 1 | From the start, 2 items |
[-2:] | -2, -1 | The last two |
[::2] | 0, 2, 4 | Every second item |
[::-1] | 5 → 0 | A negative step: reversed |
[2:100] | 2 until the end | Going out of range does not throw an error |
card_number[-4:] | The last 4 | Slicing also works on strings |
"*" * 12 | — | Repeating text 12 times |
Another example
The famous bug — two names for the same list. week-2/aliasing.py:
original = ["milk", "bread"]
same = original
copy = original.copy()
same.append("eggs")
print("original:", original)
print("same: ", same)
print("copy: ", copy)
print("same == original:", same == original)
print("same is original:", same is original)
print("copy == original:", copy == original)
print("copy is original:", copy is original)
name = "dana"
other = name
other = other.upper()
print("name:", name, "| other:", other)Output:
original: ['milk', 'bread', 'eggs']
same: ['milk', 'bread', 'eggs']
copy: ['milk', 'bread']
same == original: True
same is original: True
copy == original: False
copy is original: False
name: dana | other: DANABreakdown:
same = original— no new list was created. There are now two names for the same list.same.append("eggs")— we changed the list itself (it is Mutable), sooriginal“changed” too. In fact it did not change separately, it is the same list.copy = original.copy()— a new list with the same items, which is not affected by the change.copy == original→False, because after the append the values are already different. Before the append it wasTrue, butiswas stillFalse.- Strings are Immutable:
other.upper()created a new string, andotherwas attached to it.namewas not affected. With Immutable, this bug cannot happen.
Mutable as a function parameter
Exactly the same principle applies to functions. The parameter is another “sticker” on the same object:
def add_bonus(grades):
grades.append(100)
my_grades = [80, 90]
add_bonus(my_grades)
print(my_grades) # [80, 90, 100] - the caller's list changed!Sometimes this is desired, and sometimes it is a bug. If a function is not supposed to change its input, work on a copy: grades = grades.copy(), or build a new list and return it.
Common mistake
Mistake 1: b = a to “back up” a list before changing it.
backup = cart
cart.clear()
print(backup) # [] - the "backup" is empty too!The fix: backup = cart.copy().
Mistake 2: is for comparing values.
a = [1, 2, 3]
b = [1, 2, 3]
if a is b:
print("same")Nothing is printed. Two lists that are equal in value are two separate objects. For values, always ==.
(With small numbers and short strings, is “works by chance”, because Python reuses small objects. With big numbers it no longer happens. Do not rely on it.)
Mistake 3: forgetting that str methods do not change the string.
email = " Dana@Mail.COM "
email.strip().lower()
print(email) # still " Dana@Mail.COM "A string is Immutable, and the new result was thrown away. The fix: email = email.strip().lower().
Mistake 4: thinking stop is included. items[1:3] returns 2 items (indexes 1 and 2), not 3.
Practice
The exercises use only what was taught in this lesson and the lessons before it. Try on your own first, and only then open the solution.
Exercise 1: Slicing
letters = ["a", "b", "c", "d", "e", "f"]Using Slicing only, print: the first three, the last two, every second item, and the list reversed.
Expected output:
['a', 'b', 'c']
['e', 'f']
['a', 'c', 'e']
['f', 'e', 'd', 'c', 'b', 'a']Show solution
letters = ["a", "b", "c", "d", "e", "f"]
print(letters[:3])
print(letters[-2:])
print(letters[::2])
print(letters[::-1])['a', 'b', 'c']
['e', 'f']
['a', 'c', 'e']
['f', 'e', 'd', 'c', 'b', 'a'][start:stop:step] — the stop is not included. [::-1] returns a reversed copy — the original did not change.
Exercise 2: Hiding a card number
Write a function mask_card(number) that receives a card number as a string, and returns asterisks instead of all the digits except the last four.
print(mask_card("4580123498761234"))Expected output:
************1234Show solution
def mask_card(number):
hidden = len(number) - 4
return "*" * hidden + number[-4:]
print(mask_card("4580123498761234"))************1234Slicing also works on strings: number[-4:] is the last four digits.
Exercise 3: The backup that emptied
cart = ["milk", "bread"]
backup = cart
cart.clear()
print(backup)- What will be printed, and why?
- Fix it so that the backup is kept.
Show solution
[]will be printed.backup = cartis not a copy — both names point to the same list. Emptying it throughcartis also visible throughbackup.- Create a copy:
cart = ["milk", "bread"]
backup = cart.copy()
cart.clear()
print(backup)
print(backup is cart)['milk', 'bread']
Falseis checks whether it is the same object — after copy() it no longer is.
Exercise 4: Mutable as a parameter
def add_bonus(grades):
grades.append(100)
def shout(name):
name = name.upper()
my_grades = [80, 90]
my_name = "dana"
add_bonus(my_grades)
shout(my_name)
print(my_grades)
print(my_name)Predict what will be printed, and explain the difference between the two functions.
Show solution
def add_bonus(grades):
grades.append(100)
def shout(name):
name = name.upper()
my_grades = [80, 90]
my_name = "dana"
add_bonus(my_grades)
shout(my_name)
print(my_grades)
print(my_name)[80, 90, 100]
dana- A list is Mutable:
appendchanged the same list thatmy_gradespoints to. - A string is Immutable:
name.upper()created a new string, and the assignment only changed where the localnamepoints.my_namewas not affected.
Exercise 5: A palindrome and pages
- Write
is_palindrome(text)— whether the word reads the same from both directions (ignoring capital letters). - Write
paginate(items, page, page_size)that returns the items of a certain page (the first page is 1).
print(is_palindrome("Level"))
print(is_palindrome("hello"))
print(paginate(["a", "b", "c", "d", "e"], 2, 2))
print(paginate(["a", "b", "c", "d", "e"], 3, 2))Expected output:
True
False
['c', 'd']
['e']Show solution
def is_palindrome(text):
cleaned = text.lower()
return cleaned == cleaned[::-1]
def paginate(items, page, page_size):
start = (page - 1) * page_size
return items[start:start + page_size]
print(is_palindrome("Level"))
print(is_palindrome("hello"))
print(paginate(["a", "b", "c", "d", "e"], 2, 2))
print(paginate(["a", "b", "c", "d", "e"], 3, 2))True
False
['c', 'd']
['e']Slicing does not fail when the stop is larger than the length of the list — it simply returns what there is. That is why the last page returns ['e'].
Summary
- Slicing
[start:stop:step]returns a new sequence.stopis not included,[::-1]reverses, and there is noIndexError. - Immutable (
int,float,str,tuple,bool): every “change” creates a new object. Mutable (list,dict,set): changes in place. - A variable is a reference.
b = a= two names for the same object, not a copy. A copy:.copy()or[:]. - A function that receives a list/dict can change the original of whoever called it.
=== the same value.is= the same object. ForNone, useis None.