Lesson 06 / 16
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.
Contents
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.
"""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.
"""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.
"""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.
"""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 itemcreates 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 thelist()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 kis 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.
To keep your progress and take notes, Log in
My notes
Log in to take notes.