Hi, I have this idea that I would like to discuss and maybe find a sponsor for the PEP, if you agree that this is PEPable.
PEP: TBD
Title: Slice Views for Python Sequences
Author: Juliano Fischer Naves julianofischer@gmail.com
Status: Draft
Abstract
This PEP proposes a standard “slice view” facility for Python sequences, analogous in spirit to Go’s slices and NumPy views. A slice view presents a live window into an existing sequence: reads and writes reflect the underlying sequence, view-to-view slicing composes cheaply, and no data is copied unless explicitly requested. Existing slicing semantics remain unchanged; the feature is opt-in via a new standard type and protocol.
Motivation
- Performance and memory: Today,
seq[a:b:c]typically creates a new sequence, copying elements. For large data pipelines, this is costly. A view avoids copying while enabling idiomatic slicing. - Expressiveness: NumPy users are accustomed to view semantics; Python lacks a general-purpose equivalent beyond
memoryviewfor bytes-like objects. - Interoperability: Libraries often implement their own view wrappers. A standard type and protocol enable zero-copy interop and predictable behavior.
- Ergonomics: Go slices and similar constructs make sliding windows, subranges, and queue-like operations natural and efficient.
Rationale and non-goals
- Non-goal: Change existing
__getitem__slice semantics for built-ins likelistorstr. That would be a large backward-incompatible change. - Goal: Provide a standard viewing mechanism that:
- Works with any
collections.abc.Sequence(andMutableSequencefor writable views). - Composes slices in O(1).
- Reflects mutations of the underlying sequence when possible and well-defined.
- Allows types to opt into more efficient views via a small protocol.
- Works with any
- Relation to existing facilities:
memoryviewremains the preferred solution for the buffer protocol and contiguous bytes-like memory (e.g.,bytes,bytearray,array,numpyvia PEP 3118).- Slice views target generic sequences where the buffer protocol is not applicable (e.g., lists of Python objects, custom sequences).
Specification
New built-in: sliceview
- Constructor:
sliceview(base, start=None, stop=None, step=None)creates a view overbase, applying an initial slice. - Helper:
view(obj) -> sliceviewreturns an identity view overobj. Available inbuiltins. - Composability:
sliceviewimplements the sequence protocol. Indexing and slicing on asliceviewreturn elements or anothersliceviewrespectively, without copying.
Protocol for efficient producer types
- New optional dunder:
__sliceview__(self, slc: slice) -> sliceview | NotImplemented- If present,
sliceview(base, slc)callsbase.__sliceview__(slc)first. Types can return a specialized view (e.g., contiguous block, rope node) orNotImplementedto fall back to the generic wrapper. - For bytes-like objects supporting PEP 3118,
sliceviewmay delegate tomemoryviewinternally when that is more efficient and safe, but the Python-level type remainssliceview.
- If present,
Data model and semantics
- Identity and lifetime:
sv.basereferences the underlying sequence. Keeping the view alive keeps the base alive.
- Read semantics:
sv[i]maps tobase[start + i*step](with negativesteppermitted). Index normalization uses the view’s logical length.sv[:]returns a newsliceviewpointing at the same base (O(1)).
- Write semantics:
- If the base is a
MutableSequence,sv[i] = xforwards to the element mapping above. - Extended assignment
sv[i:j:k] = iterableforwards element-wise. Size-changing slice assignment raisesTypeErroron a view unless the base’s own__setitem__for slice with step 1 is used and the view’s step is 1 and contiguous (see “Resizing behavior”).
- If the base is a
- Resizing behavior:
- Resizing operations on the base (insert/delete) are visible in the view and shift logical indices as they do for ordinary indexing. This is analogous to iterators observing live mutation but not crashing.
- Direct resizing through the view is restricted to avoid ambiguous semantics:
- Allowed:
sv[a:b] = iterableonly ifstep == 1and the assignment length matches(b - a). OtherwiseTypeError. - Methods that conceptually resize (e.g.,
append,insert,extend,clear) are not provided onsliceview. Users should call them on the base.
- Allowed:
- Length and membership:
len(sv)computes from the current base length and the view parameters. It reflects base mutations.x in sviterates over the view.
- Hashing and equality:
sliceviewis unhashable.- Equality compares elementwise to other sequences.
- Slicing a view:
sv[p:q:r]returns a newsliceviewthat composes the slice parameters in O(1), with no data movement.
- Iteration:
- Iteration walks the base through the view mapping. Mutations to the base during iteration follow existing Python iteration semantics for lists (no guarantees beyond not crashing and visiting indices that remain valid).
- Repr:
sliceview([1,2,3,4])[1:3]renders assliceview(base=<list at 0x...>, slice=1:3:1).
Standard library integration
collections.abc: AddViewABC? Non-essential. This PEP initially proposes concretesliceviewonly.operator: Addoperator.sliceview(obj, slc)convenience that mirrorsoperator.getitem.itertools: No changes required.bisect,heapq: Work on any sequence; they remain compatible but note performance implications.
Error handling and edge cases
- Negative steps are supported and compose correctly.
- Out-of-range indices follow slice normalization rules and never raise in slicing; element indexing still raises
IndexError. - If the base drops elements, indices can become invalid for some positions; accessing them raises
IndexErroras with ordinary indexing.
Examples
- Avoid copying a sublist:
sv = view(big_list)[offset:offset+window]
- In-place windowed updates:
view(samples)[i:j] = denoise(view(samples)[i:j])
- Composable slicing:
view(seq)[2:][::3][5:10]creates no intermediate copies.
- Interop with
memoryview:view(b)[1000:2000]can use a memoryview-backed fast path under the hood.
Backward compatibility
- No change to existing slicing. All new behavior is opt-in via
sliceview/view(). - Code relying on slice copies remains unaffected.
Performance considerations
- Time: Creating a
sliceviewis O(1). Indexing and iteration overhead is comparable to indexing the base plus simple arithmetic. For tight loops, microbenchmarks should compare favorably to slice-copy approaches when the copy would dominate. - Memory: Views hold a reference to the base plus a few integers. Large intermediate slices avoid allocation and element reference bumps.
Security and safety
- Views prolong the lifetime of their base. This may unintentionally retain large structures; tooling should document memory implications.
- For types with non-stable element addresses (most Python objects), this is no worse than ordinary indexing; the PEP makes no new pointer-stability guarantees.
Rejected alternatives
- Changing built-in slicing to return views: backward-incompatible for decades of code relying on copies.
- Overloading
sliceobjects with view semantics: clarity suffers and still wouldn’t enable opt-in behavior without breaking changes. - Only blessing third-party libraries: misses the unifying standardization benefit.
Open issues for discussion
- Write resizing via views:
- Permit size-changing slice assignment when
step == 1? Current draft forbids through the view to keep semantics predictable.
- Permit size-changing slice assignment when
- ABC or Protocol:
- Should we define a
typing.ProtocollikeSupportsSliceViewwith__sliceview__for static typing?
- Should we define a
- bytes-like unification:
- When the base supports the buffer protocol, should
sliceviewdelegate tomemoryviewor always remain distinct but possibly sharing implementation?
- When the base supports the buffer protocol, should
Pure Python illustrative reference implementation
This is a minimal, correct but not optimized prototype that demonstrates the semantics.
from collections.abc import Sequence, MutableSequence
def view(base):
return sliceview(base)
class sliceview(Sequence):
__slots__ = ("base", "_start", "_stop", "_step")
def __init__(self, base, start=None, stop=None, step=None):
self.base = base
if isinstance(start, slice) and stop is None and step is None:
sl = start
else:
sl = slice(start, stop, step)
# Normalize using current length
b_len = len(base)
start, stop, step = sl.indices(b_len)
self._start, self._stop, self._step = start, stop, step
def __repr__(self):
s = f"{self._start}:{self._stop}:{self._step}"
return f"sliceview(base={type(self.base).__name__}, slice={s})"
def __len__(self):
# Compute length like range does
start, stop, step = self._start, self._stop, self._step
if (step > 0 and start >= stop) or (step < 0 and start <= stop):
return 0
n = (abs(stop - start) + abs(step) - 1) // abs(step)
return n
def _map_index(self, i):
# Support negative indices relative to the view
if i < 0:
i += len(self)
if i < 0 or i >= len(self):
raise IndexError("sliceview index out of range")
return self._start + i * self._step
def __getitem__(self, key):
if isinstance(key, slice):
# Compose slices: map to base indices and create new view
start = key.start
stop = key.stop
step = key.step
# Translate key to absolute on the view
vlen = len(self)
sstart, sstop, sstep = (slice(start, stop, step)).indices(vlen)
# Compose with base mapping
base_start = self._start + sstart * self._step
base_step = self._step * sstep
# Compute base_stop from count
count = (abs(sstop - sstart) + abs(sstep) - 1) // abs(sstep) if vlen else 0
if count == 0:
# Empty view: choose a canonical empty slice on base
return sliceview(self.base, 0, 0, 1)
base_stop = base_start + count * base_step
return sliceview(self.base, base_start, base_stop, base_step)
else:
return self.base[self._map_index(key)]
# Optional: enable write-through for mutable sequences
def __setitem__(self, key, value):
base = self.base
if not isinstance(base, MutableSequence):
raise TypeError("underlying sequence is not mutable")
if isinstance(key, slice):
# Restrict to non-resizing, step==1 for clarity
sstart, sstop, sstep = key.indices(len(self))
if sstep != 1 or sstop - sstart != len(value):
raise TypeError("resizing or stepped slice assignment via view is not supported")
# Map each index and assign
bi = self._start + sstart * self._step
for x in value:
base[bi] = x
bi += self._step
else:
base[self._map_index(key)] = value
def __contains__(self, x):
for y in self:
if y == x:
return True
return False
Typing
- A
typing.SupportsSliceViewprotocol could standardize__sliceview__.sliceviewitself implementsSequence, and conditionally aMutableSequence-like subset for write-through, though it intentionally avoids resizing methods.
Reference documentation additions
builtins.view(obj): return a view proxy over any sequence-like object.class sliceview: describe constructor, behaviors, and caveats.
Migration strategy
- None required. Libraries can adopt
sliceviewto avoid internal copies and to expose efficient APIs. Tutorials should recommendview(seq)[i:j]when a non-copying window is needed.
Acknowledgements
- Inspired by Go slices and Python’s
memoryview, as well as longstanding community discussions around zero-copy sequence windows.