---
title: Slicing
source: 'https://academia.sh/en/courses/python-data-structures/slicing'
course: 'Data Structures and Functional Tools'
language: en
updated: '2026-08-17T18:10:25+00:00'
license: 'CC BY-SA 4.0'
---

# Slicing

All eight slice forms create 0 new items and return a new container; a nested list produces 4 items and both inner lists are shared in the slice copy — the outer container is new, the contents are old.

The previous lesson's rotation measurement took two slices, and the result
was a new container. A slice passed there as a detail; on its own it is
this course's most misunderstood operation. This lesson carries one
question through: when a slice is taken, what gets copied — the container
itself, its contents, or both?

## A Slice Wants a Container

**Slicing** syntax takes three parts: start, stop, step. All three are
optional and fall back to the container's ends and to one when omitted.
The Python Fundamentals course measured this syntax as an abbreviation;
there, which method it called was counted — here, how many containers
that call builds.

An index wants a single slot, and asking for one that does not exist is a
fault; a slice wants a range, and the part beyond the container is
silently clipped.

- **CO65** — All eight forms work on the same six-item source; items are
  created once, and the counter resets right before each slice. "Items
  from source" is found by searching each slice item against the source
  with `is`.
- **CO66** — Slice assignment and deletion proceed sequentially on a
  single list; the second row continues from the first's state.

```python
"""Reading a slice: every slice builds a new container, none creates a new item."""

COUNTER = {"created": 0}


class Item:
    def __init__(self, value):
        COUNTER["created"] += 1
        self.value = value

    def __repr__(self):
        return f"Item({self.value})"


def reset():
    COUNTER["created"] = 0


def created():
    return COUNTER["created"]


SOURCE = [Item(i) for i in range(6)]

FORMS = (("k[1:4]", lambda k: k[1:4]), ("k[:3]", lambda k: k[:3]),
         ("k[3:]", lambda k: k[3:]), ("k[::2]", lambda k: k[::2]),
         ("k[::-1]", lambda k: k[::-1]), ("k[:]", lambda k: k[:]),
         ("k[4:100]", lambda k: k[4:100]), ("k[10:20]", lambda k: k[10:20]))

print(f"{'slice':<12s}{'new items':>9s}{'slots':>6s}{'same container':>16s}"
      f"{'items from source':>19s}{'content':>26s}")
for label, take in FORMS:
    reset()
    piece = take(SOURCE)
    from_source = all(any(x is y for y in SOURCE) for x in piece)
    print(f"{label:<12s}{created():>9d}{len(piece):>6d}"
          f"{str(piece is SOURCE):>16s}{str(from_source):>19s}"
          f"{str([o.value for o in piece]):>26s}")

print()
try:
    SOURCE[10]
except IndexError as e:
    print(f"out-of-range index: {type(e).__name__} | "
          f"out-of-range slice: {[o.value for o in SOURCE[10:20]]}")

print()
target = list(SOURCE)
before = target
reset()
target[1:3] = [Item(9), Item(9), Item(9)]
print(f"slice assignment: created {created()} | same container "
      f"{target is before} | slots {len(before)} -> {[o.value for o in target]}")

reset()
del target[::2]
print(f"slice deletion:   created {created()} | same container "
      f"{target is before} | slots {len(target)} -> {[o.value for o in target]}")

print()
text = "north-2031"
print(f"string slice {text[6:]!r} | tuple slice "
      f"{tuple(o.value for o in SOURCE)[1:4]}")
```

```
slice       new items slots  same container  items from source                   content
k[1:4]              0     3           False               True                 [1, 2, 3]
k[:3]               0     3           False               True                 [0, 1, 2]
k[3:]               0     3           False               True                 [3, 4, 5]
k[::2]              0     3           False               True                 [0, 2, 4]
k[::-1]             0     6           False               True        [5, 4, 3, 2, 1, 0]
k[:]                0     6           False               True        [0, 1, 2, 3, 4, 5]
k[4:100]            0     2           False               True                    [4, 5]
k[10:20]            0     0           False               True                        []

out-of-range index: IndexError | out-of-range slice: []

slice assignment: created 3 | same container True | slots 7 -> [0, 9, 9, 9, 3, 4, 5]
slice deletion:   created 0 | same container True | slots 3 -> [9, 9, 4]

string slice '2031' | tuple slice (1, 2, 3)
```

## Eight Slices, Zero Items

The first column is 0 in all eight rows: taking a slice creates no items.
The third is False in all eight: every slice is a new container on a
list, even a full one. The fourth is True in all eight: every slice item
is the source's item. Together, the three give this lesson's
one-sentence finding — a slice renews the container, not its contents.
Step selects content but does not change the table; `k[::2]` takes three
slots, `k[::-1]` six, both creating 0 items.

The last two rows show where a slice diverges from an index: a slot
outside the container raises `IndexError`; a range outside it,
`k[4:100]` gives two items, `k[10:20]` gives zero, neither a fault. A
range is a different question from a specific slot.

The lower block's three lines are a slice's other face: reading births a
new container, writing mutates one in place — `target is before` stays
True. That measurement opens up in "Writing Is Not Copying."

## How Deep a Copy Goes

The source measured so far was a flat list. The real question shows up
with a container inside a container: taking a slice of a nested list
makes the outer container new — measured already. What about the inner
containers?

- **CO67** — The nested source carries two inner lists and four leaf
  items, confirmed by the measurement's first row. The mutation test
  happens at the end; everything before it takes the source untouched.
- **CO68** — A deep copy does not pass objects through the constructor, so
  the creation counter cannot see it; a new object's existence is counted
  by identity — copy leaves not found in the source with `is`.

```python
"""A copy is one layer deep: the outer container is new, the contents are old."""

import copy

COUNTER = {"created": 0}


class Item:
    def __init__(self, value):
        COUNTER["created"] += 1
        self.value = value

    def __repr__(self):
        return f"Item({self.value})"


def reset():
    COUNTER["created"] = 0


def created():
    return COUNTER["created"]


def nested_lists():
    reset()
    nested = [[Item(1), Item(2)], [Item(3), Item(4)]]
    piece = nested[:]
    list_copy = list(nested)
    shared = sum(1 for a, b in zip(nested, piece) if a is b)
    return created(), shared, piece[0] is nested[0], list_copy[0] is nested[0]


nested_created, shared, piece_same, list_same = nested_lists()
print(f"items created in the nested list {nested_created}, inner lists shared "
      f"in the slice copy {shared}/2")
print(f"piece[0] is nested[0] -> {piece_same}, list(nested)[0] is nested[0] -> {list_same}")


def leaves(k):
    return [o for inner_list in k for o in inner_list]


def distinct_leaves(copy_, source):
    """Count of leaves in the copy not found in the source."""
    old = leaves(source)
    return sum(1 for o in leaves(copy_) if not any(o is y for y in old))


print()
reset()
nested = [[Item(1), Item(2)], [Item(3), Item(4)]]
print(f"items created while building the source: {created()} | "
      f"inner lists {len(nested)} | leaves {len(leaves(nested))}")

COPIES = (("k[:]", nested[:]), ("list(k)", list(nested)),
          ("copy.copy(k)", copy.copy(nested)),
          ("copy.deepcopy(k)", copy.deepcopy(nested)))

print(f"{'copy form':<20s}{'distinct leaves':>16s}{'outer same':>12s}"
      f"{'inner same':>13s}{'leaf same':>11s}")
for label, c in COPIES:
    print(f"{label:<20s}{distinct_leaves(c, nested):>16d}{str(c is nested):>12s}"
          f"{str(c[0] is nested[0]):>13s}{str(c[0][0] is nested[0][0]):>11s}")

print()
shallow = COPIES[0][1]
deep = COPIES[3][1]
nested[0][0].value = 99
print(f"leaf mutated in the source -> nested[0][0] = {nested[0][0]}")
print(f"  shallow[0][0] = {shallow[0][0]} | deep[0][0] = {deep[0][0]}")
nested.append([Item(5)])
print(f"inner list added to the source -> source slots {len(nested)} | "
      f"shallow slots {len(shallow)} | deep slots {len(deep)}")
```

```
items created in the nested list 4, inner lists shared in the slice copy 2/2
piece[0] is nested[0] -> True, list(nested)[0] is nested[0] -> True

items created while building the source: 4 | inner lists 2 | leaves 4
copy form            distinct leaves  outer same   inner same  leaf same
k[:]                               0       False         True       True
list(k)                            0       False         True       True
copy.copy(k)                       0       False         True       True
copy.deepcopy(k)                   4       False        False      False

leaf mutated in the source -> nested[0][0] = Item(99)
  shallow[0][0] = Item(99) | deep[0][0] = Item(1)
inner list added to the source -> source slots 3 | shallow slots 2 | deep slots 2
```

## The Outer Container Is New, the Contents Are Old

The first two lines pay off this course's third claim exactly. The nested
list creates 4 items; in the slice copy, both inner lists are shared —
`piece[0] is nested[0]` is True — same for the `list()` copy. A copy is
one layer deep.

The lower table extends this to four copy forms and puts three in the
same place. These three are shallow copy: a slice, the list constructor,
the copy function. In all three, outer is separate, inner list is the
same, leaf is the same, distinct-leaf count is 0. The fourth diverges: in
a deep copy, all three columns are separate and distinct-leaf count is 4.
That 0 against 4 is the whole difference between the two spellings.

The last block shows what it means. When a leaf's inside is mutated in
the source, the slice copy sees it — `shallow[0][0]` becomes `Item(99)`
too — the deep copy does not. When a new inner list is added to the
source, both stay at 2 slots, the source goes to 3. What's shared is the
objects the outer container's slots point to, not the container itself;
a shallow copy is independent by exactly one layer.

## Writing Is Not Copying

The previous section read a slice and copied one layer. Writing to a
slice reverses the measure: no container gets built, identity is
preserved, length changes. This section measures that opposite, and the
lesson's claim's limit.

- **CO66 (continued)** — The write measurement proceeds sequentially on a
  single list. A deleted item's survival is tested with a separate name
  taken before the delete; the immutable-container test uses three
  separate, untouched containers.

```python
"""Writing to a slice is a mutation, not a copy; a full slice on an immutable container builds no new container."""

COUNTER = {"created": 0}


class Item:
    def __init__(self, value):
        COUNTER["created"] += 1
        self.value = value


def reset():
    COUNTER["created"] = 0


def created():
    return COUNTER["created"]


lst = [Item(i) for i in range(6)]
identity = lst

for label, d in (("k[4:1:-1]", lst[4:1:-1]), ("k[1:4:-1]", lst[1:4:-1]),
                 ("k[::-2]", lst[::-2])):
    print(f"{label:<10s} slots {len(d)} -> {[o.value for o in d]}")

print()
reset()
lst[1:4] = [Item(7)]
print(f"k[1:4] = single item: created {created()} | same container {lst is identity} | "
      f"slots 6 -> {len(lst)} | {[o.value for o in lst]}")

second = lst[1]
held = lst[1:3]
reset()
del lst[1:3]
print(f"del k[1:3]:      created {created()} | same container {lst is identity} | "
      f"slots {len(lst)} | {[o.value for o in lst]}")
print(f"  deleted item lives under separate name -> {second is held[0]} | "
      f"stayed in container -> {any(second is x for x in lst)}")

print()
for label, container in (("list", [1, 2, 3]), ("tuple", (1, 2, 3)), ("string", "abc")):
    print(f"{label:<7s} k[:] is k -> {str(container[:] is container):<6s} k[:2] is k -> {container[:2] is container}")
```

```
k[4:1:-1]  slots 3 -> [4, 3, 2]
k[1:4:-1]  slots 0 -> []
k[::-2]    slots 3 -> [5, 3, 1]

k[1:4] = single item: created 1 | same container True | slots 6 -> 4 | [0, 7, 4, 5]
del k[1:3]:      created 0 | same container True | slots 2 | [0, 5]
  deleted item lives under separate name -> True | stayed in container -> False

list    k[:] is k -> False  k[:2] is k -> False
tuple   k[:] is k -> True   k[:2] is k -> False
string  k[:] is k -> True   k[:2] is k -> False
```

The top three rows measure the negative step. `k[::-1]` already created 0
items and gave a new container in the main table; here it is the empty
slice's counterpart under a negative step. `k[4:1:-1]` gives three slots
while `k[1:4:-1]` gives 0: same bounds, opposite direction — a
backward-going slice needs its start greater than its stop.

The middle block measures writing. `k[1:4] = single item` creates 1
object — we built that one — and `lst is identity` stays True: the
container did not change, its inside did. Slots drop from 6 to 4; three
were taken and one put in, so assignment does not require matching
counts. `del k[1:3]` completes the axis: 0 created, identity preserved,
slots down to 2. On a list, `k[:]` returns a new container; assignment
and deletion mutate the existing one in place. The deleted item does not
vanish either: under the name taken before the delete, it is still the
same object (True), no longer in the container (False) — deletion
removes the slot.

The lower block is the claim's limit. On a list, `k[:] is k` is False; on
a tuple and a string, True — there, a full slice builds no new
container, it hands back the existing one. "The outer container is new"
does not hold for immutable types, since duplicating a container whose
content cannot change has no measurable payoff. `k[:2] is k` is False on
all three; the limit is specific to a full slice. The claim narrows — a
slice renews a mutable container, never its contents.

## Who Does the Clipping

All three measurements showed what a slice does; none showed who does it.
Clipping an out-of-range range looked like a container's behavior, but
is not: behind the colon syntax stands a named object.

- **CO69** — A single slice recipe object is built and used on three
  separate containers; results are compared against the same recipe's
  spelled-out equivalent.
- **CO70** — Resolved bounds are read from the recipe's own method, never
  computed by hand; slot count is also taken from the slice's length, and
  the two confirm each other.

```python
"""A slice recipe is an object: the same recipe is used on three containers, bounds resolve from it."""

SOURCE = list(range(6))
CONTAINERS = (("list", SOURCE), ("tuple", tuple(range(6))), ("string", "norths"))

RECIPE = slice(1, 4)
print(f"a recipe is an object: {type(RECIPE).__name__} | "
      f"start {RECIPE.start} stop {RECIPE.stop} step {RECIPE.step}")
print(f"{'container':<10s}{'k[recipe]':<14s}{'same as k[1:4]'}")
for label, container in CONTAINERS:
    print(f"{label:<10s}{str(container[RECIPE]):<14s}{container[RECIPE] == container[1:4]}")
print(f"one recipe object used on {len(CONTAINERS)} different containers")

print()
print(f"{'spelling':<12s}{'resolved bounds':<20s}{'slots'}")
for label, t in (("k[1:4]", slice(1, 4)), ("k[:]", slice(None, None)),
                 ("k[::-1]", slice(None, None, -1)), ("k[-3:]", slice(-3, None)),
                 ("k[4:100]", slice(4, 100)), ("k[10:20]", slice(10, 20))):
    print(f"{label:<12s}{str(t.indices(len(SOURCE))):<20s}{len(SOURCE[t])}")
```

```
a recipe is an object: slice | start 1 stop 4 step None
container k[recipe]     same as k[1:4]
list      [1, 2, 3]     True
tuple     (1, 2, 3)     True
string    ort           True
one recipe object used on 3 different containers

spelling    resolved bounds     slots
k[1:4]      (1, 4, 1)           3
k[:]        (0, 6, 1)           6
k[::-1]     (5, -1, -1)         6
k[-3:]      (3, 6, 1)           3
k[4:100]    (4, 6, 1)           2
k[10:20]    (6, 6, 1)           0
```

The upper block surfaces the object behind the syntax. `k[1:4]` does not
pass two numbers to the container, it passes a single recipe with three
fields; an unspecified one stays `None`. The same recipe is used on three
separate containers and gives the same result as the spelled-out syntax
in all three. The recipe is not bound to a container: it knows neither
which one it'll apply to nor that container's length.

The lower block shows where clipping happens. Once the recipe learns the
length of the container it applies to, it resolves to three concrete
numbers: `k[:]` becomes `(0, 6, 1)` and unspecified fields fill in;
`k[-3:]` becomes `(3, 6, 1)`, the negative number subtracted from the
length; `k[4:100]` becomes `(4, 6, 1)`, 100 clipped to 6; `k[10:20]`
becomes `(6, 6, 1)` and comes up empty.

An out-of-range range not counting as a fault is not a behavior choice,
it is a measured step — clipping happens resolving the recipe, before
ever entering the container. The negative-step recipe giving
`(5, -1, -1)` is a product of the same resolution, and it explains
`k[1:4:-1]`'s emptiness.

## Summary

- All eight slice forms create 0 items, all eight return a new container
  on a list, and all eight have items from the source — a full slice
  included.
- An out-of-range index raises `IndexError`, an out-of-range slice gets
  clipped, `k[1:4:-1]` gives 0 slots. Clipping happens resolving the
  container-independent recipe, before entering the container:
  `k[10:20]` → `(6, 6, 1)`.
- Writing to a slice works on the same container and changes its length:
  `k[1:4] = single item` creates 1 object and drops slots from 6 to 4,
  `del k[1:3]` creates 0 and drops them to 2; identity is preserved in
  both, and a deleted item lives on if another name holds it.
- A nested list creates 4 items; in the slice copy, both inner lists are
  shared (`piece[0] is nested[0]` → True), same for the `list()` copy —
  the outer container is new, the contents are old. Shallow copy's three
  spellings give 0 distinct leaves, deep copy gives 4.
- The claim's limit: `k[:] is k` is False on a list, True on a tuple and
  a string. A full slice builds no new container on an immutable one — a
  slice renews a mutable container, never its contents.

## Next Step

Every copy in this lesson was one layer deep, and building even that one
layer touched every slot: taking a slice of a six-item container means
reading six slots and writing them to a new one, a thousand on a
thousand-item container. Even with 0 items created, slots touched equal
the container's length.

Is rebuilding the container to pass over its items required, though? The
next lesson asks that in reverse: is there a way to reach a container's
items in order, without building a new container and without touching
the one at hand? The Python Fundamentals course measured the protocol's
calling side; the next lesson moves to the building side, and its first
job is this: writing the contract between `iter` and `next` by hand.
