2025, Oct 18 00:00
Python list slice assignment explained: why the right-hand side makes a new list, not an in-place move
Learn how Python list slice assignment works: the RHS builds a new list, not an in-place memmove. CPython disassembly and iterable-based alternatives inside.
When reassigning one slice of a Python list with another slice from the same list, a natural question arises: does the right-hand side create a temporary list, or is there an in-place optimization that avoids extra allocation? Understanding what really happens helps you write predictable, efficient code and avoid relying on optimizations that aren’t actually guaranteed.
Problem setup
The core operation looks like this:
L[a:b] = L[c:d]Python’s tutorial states the following about slicing:
All slice operations return a new list containing the requested elements.
But it doesn’t explicitly say whether assigning a slice from the same list uses a temporary or can be optimized away.
Minimal code that triggers the question
The following function reads a slice and writes it back to another slice of the same list:
def demo_fn():
    lst_ref = [1, 2, 3, 4]
    lst_ref[0:1] = lst_ref[2:3]
    return lst_refA slightly modified version makes the intermediate even more obvious by extending the right-hand side:
def demo_plus():
    buf = [1, 2, 3, 4]
    buf[0:1] = buf[2:3] + [5, 6]
    return bufWhat actually happens and why
The right-hand side is evaluated first, and that evaluation creates a new list. In other words, the slice expression on the right returns a fresh list with the requested elements before the left-hand side is updated. Evidence for this can be seen by disassembling such code in CPython: the slice read happens via BINARY_SLICE and the write via STORE_SLICE, with the list materialized in between. The presence of distinct read and write operations makes it clear there isn’t a special-case “memmove” of list internals happening.
There are two practical reasons for this behavior. First, Python list elements are independent references to objects. Reusing the same objects in another position means reference counts must be updated appropriately; blindly copying raw memory would not adjust reference counts and would break correctness. Second, slice assignments can change the length of the list, forcing element shifts. An “in-place” memory move for general slice reassignment quickly becomes complex and error-prone, especially when sources and targets overlap.
It’s also important that Python can’t rely on list-specific tricks in the general case. The object on the left side might not even be a built-in list; types can customize slicing and assignment. That flexibility makes cross-object, cross-type, special-case bytecode optimization far less feasible.
Solution and practical options
The behavior to rely on is straightforward: the right-hand slice expression produces a new list, and that list is then assigned to the target slice. If your goal is to avoid creating a separate list explicitly, one approach is to provide an iterable directly, such as a generator that yields the desired elements by index rather than taking a slice first. That way, you still perform slice assignment but feed it from an iterator rather than from a pre-built list.
seq = [...]  # some existing list
def iter_window(source, lo, hi):
    for pos in range(lo, hi):
        yield source[pos]
seq[0:10] = iter_window(seq, 10, 20)This selects elements by index without first constructing a list via slicing. As execution overhead in CPython has been reduced over time, feeding slice assignment from a generator can be a viable pattern, and it is at least amenable to optimization while keeping semantics clear.
There is a more extreme idea involving direct memory operations to “paste” pointers using ctypes. The concept would involve first creating a source slice so reference counts are correct, ensuring the target range holds an immortal object like None to avoid premature decref, then overwriting internal pointers with ctypes.memmove and cleaning up the source slice. While possible in principle, this is a lot of work to reimplement the guarantees Python already provides, with minimal to no practical gain and much higher risk.
Finally, note that this discussion is about Python lists. Arrays of raw numeric data are different. For numeric workloads, standard library arrays and, more commonly, NumPy arrays store raw data, not object references. That allows fast, contiguous memory operations. In NumPy, slice assignment between arrays can detect that both sides are arrays and perform efficient copying, and getting a slice frequently returns a view rather than copying data. If your use case is numeric, that’s usually the right tool.
Why this matters
Expectations about mutation, temporaries, and performance guide how you structure code. Assuming an in-place “memmove-style” operation for list slices can lead to incorrect mental models and surprises. Knowing that the right-hand slice becomes a new list clarifies both time and memory costs and helps you decide when a generator-based iterable, a simple for-loop, or a domain-appropriate array type is a better fit.
Takeaways
Slice assignment with lists evaluates the right-hand side first and produces a new list. CPython’s disassembly shows a distinct slice read followed by a slice write, not a fused in-place move. Because list elements are references whose counts must be adjusted, and because slice assignments can resize lists, a blanket in-place optimization isn’t how Python lists operate. If you need to avoid constructing a list on the right-hand side, yield elements from an iterator. If you need raw, contiguous memory semantics for numeric data, reach for arrays designed for that job, typically NumPy. Above all, write with the actual semantics in mind so your code remains both correct and clear.
The article is based on a question from StackOverflow by user29120650 and an answer by jsbueno.