You have written items[n:m] and trusted that items[:n] plus items[n:] covers every element exactly once. That trust is well placed — but only if you understand the interval model beneath the brackets.
Think of slices like train cars. Each car has a number, but you board at the front edge of car N and get off at the front edge of car M. The car you exit is the one you do NOT ride. That is half-open: start included, stop excluded. The beauty is that a[:n] and a[n:] partition a list cleanly — no overlap, no gap, no off-by-one.
Python slices use half-open intervals: seq[start:stop] includes start and excludes stop. That choice makes boundaries composable and length arithmetic trivial.
# [CURRENT - 3.10-3.14] Works on Python 3.xitems = list(range(10))middle = items[2:5]head = items[:5]tail = items[5:]print(middle) # [2, 3, 4]print(len(middle) == 5 - 2)print(head + tail == items)The excluded stop is why seq[:x] and seq[x:] partition without overlap or gaps. That is the real reason the convention survives: it keeps slicing algebra easy in production code.
Negative indexes count from the end. Missing bounds mean "use the natural boundary." A negative step reverses traversal direction.
# [CURRENT - 3.10-3.14] Works on Python 3.xpath = "/srv/app/events.log"print(path[-10:]) # events.logprint(path[::-1]) # gol.stneve/ppa/vrs/print(path[::-2]) # go.tnv/pa/rThe expression seq[a:b:c] creates a slice(a, b, c) object and passes it through subscription. For built-in sequences, CPython dispatches to optimized C implementations. For user-defined containers, the object passed to __getitem__ is literally a slice instance.
# [CURRENT - 3.10-3.14] Works on Python 3.xclass AuditList: def __init__(self, values): self._values = list(values) def __getitem__(self, index): print(repr(index)) return self._values[index]data = AuditList([10, 20, 30, 40])print(data[1:3]) # slice(1, 3, None), then [20, 30]Normal slicing of built-in sequences creates a new object. For lists, that new list is a shallow copy of the referenced range: the outer container is new, but the element references are reused.
# [CURRENT - 3.10-3.14] Works on Python 3.xrows = [[1], [2]]copy = rows[:]copy[0].append(99)print(copy is rows) # Falseprint(rows) # [[1, 99], [2]]That means the time and memory cost of list slicing are proportional to the slice length, not constant. On this machine, the container-only size difference is visible immediately:
# [CURRENT - 3.10-3.14] Works on Python 3.x# Example byte counts below were measured on CPython 3.12.3, 64-bit Linux.import sysrows = list(range(1000))print(sys.getsizeof(rows)) # 8056print(sys.getsizeof(rows[:100])) # 856The 100-element slice allocates a brand new list object large enough to hold 100 references. That is why repeated slicing in tight loops can be a real throughput and memory cost.
The full slice form is seq[start:stop:step]. Python normalizes weird or out-of-range values using the semantics exposed by slice.indices(length).
# [CURRENT - 3.10-3.14] Works on Python 3.xwindow = slice(-5, None, 2)print(window.indices(12)) # normalized start, stop, stepExtended slicing changes two things:
- traversal can skip elements
- assignment becomes stricter
# [CURRENT - 3.10-3.14] Works on Python 3.xnums = list(range(8))nums[2:5] = [20, 30]print(nums) # [0, 1, 20, 30, 5, 6, 7]nums[::2] = [100, 200, 300, 400]print(nums) # [100, 1, 200, 30, 300, 6, 400]When the step is not 1, the replacement iterable must have exactly the same number of elements as the targeted positions. That is because Python is replacing a strided selection, not just splicing a contiguous gap.
Slicing semantics are stable Python 3 behavior. Current project guidance targets Python 3.10-3.14. Python 3.9 and below are End-of-Life.
The exact byte counts shown here are CPython 3.12.3, 64-bit measurements taken locally. The half-open semantics are language guarantees; the memory footprint is an implementation detail.
Do not assume slices are views. List, tuple, and string slices allocate new objects. If you need zero-copy behavior over binary data, use memoryview, not slicing on bytes or bytearray.
# [CURRENT - 3.10-3.14] Works on Python 3.xdata = b"abcdefgh"print(data[2:5]) # b'cde' -> a new bytes objectSlice copying is often invisible in review because the syntax is compact. In hot paths, treat every list slice as an allocation proportional to the number of elements copied.
Use named slice objects when the indexes have domain meaning instead of algorithmic meaning.
# [CURRENT - 3.10-3.14] Works on Python 3.xinvoice = "2026-05-10CUST042PAID "DATE = slice(0, 10)CUSTOMER = slice(10, 17)STATUS = slice(17, 22)record = { "date": invoice[DATE], "customer": invoice[CUSTOMER], "status": invoice[STATUS].strip(),}print(record)Use slicing when positions are the real model. Use sequence pattern matching when the real question is structural shape and name binding. See .