# Slicing, Mutable versus Immutable, and is versus ==<no value>

## 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`.
- **`is` versus `==`** — "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:

```text
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.

```text
letters = ["a", "b", "c", "d", "e", "f"]
index:      0    1    2    3    4    5
negative:  -6   -5   -4   -3   -2   -1
```

Slicing **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:

```python
name = "dana"
name.upper()      # returns a NEW string "DANA"; name is still "dana"
name = name.upper()   # now name points to the new string
```

**Mutable** = 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.

```python
a = [1, 2, 3]
b = a          # NOT a copy! b is a second sticker on the SAME list
```

```text
a ──┐
    ├──► [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:`.

{{< box "analogy" "Analogy: twins" >}}
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.
{{< /box >}}

## Syntax

```python
# 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 number
```

## First code example

`week-2/slicing_demo.py`:

```python
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:**

```text
['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: ************1234
```

## Breaking 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`:

```python
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:**

```text
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: DANA
```

**Breakdown:**

- `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), so `original` "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 was `True`, but `is` was still `False`.
- **Strings** are Immutable: `other.upper()` created a **new** string, and `other` was attached to it. `name` was 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:

```python
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.**

```python
backup = cart
cart.clear()
print(backup)   # [] - the "backup" is empty too!
```

The fix: `backup = cart.copy()`.

**Mistake 2: `is` for comparing values.**

```python
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.**

```python
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.

<!-- exercises:start — generated by scripts/intro6/sync_site_exercises.py; edit scripts/intro6/exercises/ -->
## 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

```python
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:**

```text
['a', 'b', 'c']
['e', 'f']
['a', 'c', 'e']
['f', 'e', 'd', 'c', 'b', 'a']
```

{{< solution >}}
```python
letters = ["a", "b", "c", "d", "e", "f"]

print(letters[:3])
print(letters[-2:])
print(letters[::2])
print(letters[::-1])
```

```text
['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.
{{< /solution >}}

### 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.

```python
print(mask_card("4580123498761234"))
```

**Expected output:**

```text
************1234
```

{{< solution >}}
```python
def mask_card(number):
    hidden = len(number) - 4
    return "*" * hidden + number[-4:]


print(mask_card("4580123498761234"))
```

```text
************1234
```

Slicing also works on strings: `number[-4:]` is the last four digits.
{{< /solution >}}

### Exercise 3: The backup that emptied

```python
cart = ["milk", "bread"]
backup = cart
cart.clear()
print(backup)
```

1. What will be printed, and why?
2. Fix it so that the backup is kept.

{{< solution >}}
1. `[]` will be printed. **`backup = cart` is not a copy** — both names point to **the same** list. Emptying it through `cart` is also visible through `backup`.
2. Create a copy:

```python
cart = ["milk", "bread"]
backup = cart.copy()
cart.clear()
print(backup)
print(backup is cart)
```

```text
['milk', 'bread']
False
```

`is` checks whether it is **the same object** — after `copy()` it no longer is.
{{< /solution >}}

### Exercise 4: Mutable as a parameter

```python
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.

{{< solution >}}
```python
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)
```

```text
[80, 90, 100]
dana
```

- A list is **Mutable**: `append` changed **the same** list that `my_grades` points to.
- A string is **Immutable**: `name.upper()` created a **new** string, and the assignment only changed where the local `name` points. `my_name` was not affected.
{{< /solution >}}

### Exercise 5: A palindrome and pages

1. Write `is_palindrome(text)` — whether the word reads the same from both directions (ignoring capital letters).
2. Write `paginate(items, page, page_size)` that returns the items of a certain page (the first page is 1).

```python
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:**

```text
True
False
['c', 'd']
['e']
```

{{< solution >}}
```python
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))
```

```text
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']`.
{{< /solution >}}

<!-- exercises:end -->

## Summary

- **Slicing** `[start:stop:step]` returns a **new** sequence. `stop` is not included, `[::-1]` reverses, and there is no `IndexError`.
- **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**. For `None`, use `is None`.

<!-- docs:start — generated by scripts/intro6/site_docs.py; edit the links there -->

## Official documentation

- [Common Sequence Operations](https://docs.python.org/3/builtins/stdtypes.html#common-sequence-operations)
- [Data model — Objects, values and types](https://docs.python.org/3/reference/datamodel.html#objects-values-and-types)
- [Identity comparisons](https://docs.python.org/3/reference/expressions.html#is-not)

<!-- docs:end -->
