Why does slicing a `list` subclass return a plain `list` instead of the subclass?
answer
- the type quietly changes
- who builds the new object?
- C code cannot guess your constructor
- slicing, +, * and copy() lose it
- UserList uses self.__class__
basics
~20 sBuilt-in list operations construct their result with the concrete list type rather than with type(self), so slicing, +, * and copy() all hand back a plain list. Override those methods yourself, or wrap a list with collections.UserList.
solid answer
~40 s`list.__getitem__` with a slice, `list.__add__`, `list.__mul__` and `list.copy` are C code that allocates a fresh `list` directly; they never consult `type(self)`, and they cannot, because a subclass constructor may take any signature at all. So for `class Stack(list)`, `Stack([1, 2, 3])[0:2]` is a `list`, and any code downstream that expected a `Stack` silently loses the extra behaviour. In-place operations are the exception: `+=`, `append`, `extend` and `sort` mutate the existing object, so the type survives. Two honest fixes exist. Override every result-producing method to rebuild `type(self)(...)` — easy to get half-right, since it means `__getitem__`, `__add__`, `__radd__`, `__mul__` and `copy`. Or subclass `collections.UserList`, a pure-Python wrapper around a real list whose corresponding methods return `self.__class__(...)`.
code
python · 10 linesclass Stack(list):
def push(self, item):
self.append(item)
s = Stack([1, 2, 3])
print(type(s[0:2])) # <class 'list'>
print(type(s + [4])) # <class 'list'>
s += [4]
print(type(s)) # <class '__main__.Stack'>go deeper
Be ready to say what type(s[0:2]) prints for a list subclass and why it surprises people. Knowing that built-in operations return plain lists, and that collections.UserList exists, is enough at this level.
Explain the mechanism: the C implementation allocates a concrete list and never consults type(self), because it cannot know a subclass constructor's signature. Name which operations mutate in place and therefore keep the type.
Show the judgment call. Say when a plain list subclass is fine, when collections.UserList earns its per-call overhead, and when you would hold a list as a private attribute instead of inheriting fifteen mutators you never meant to expose.
Own the API consequence: a public type that silently degrades to list under slicing is a contract you cannot enforce. Decide whether the codebase standardises on wrapper types, and what the guidance is for library-facing collections.
## The observation Subclassing `list` looks like it works. You inherit every method, `isinstance(obj, list)` is true, and code that only appends and iterates is happy. Then someone takes a slice, and the subclass is gone: ```python class Stack(list): def push(self, item): self.append(item) s = Stack([1, 2, 3]) type(s[0:2]) # <class 'list'> type(s + [4]) # <class 'list'> type(s * 2) # <class 'list'> type(s.copy()) # <class 'list'> ``` A later `chunk.push(x)` raises `AttributeError`, usually far from the slice that caused it. ## Why the built-in behaves this way `list` is implemented in C. When `list.__getitem__` is handed a slice object it allocates a new list of the concrete built-in type and copies the item pointers into it. The same is true of `list.__add__` (`+`), `list.__mul__` (`*`) and `list.copy`. None of these ask the receiver what class it is. The reason is not laziness, it is that the built-in cannot construct your subclass safely. A subclass constructor may take any parameters at all — `Stack(name, capacity)` is perfectly legal — so there is no call the C code could make that is guaranteed to produce a valid instance. Rather than guess, the built-in returns the type it definitely knows how to build. Integers behave the same way: an `int` subclass plus one is an `int`. ## What does keep the type Anything that mutates the object you already have: - `s += [4]` goes through `list.__iadd__`, which extends in place and returns the same object. - `append`, `extend`, `insert`, `sort`, `reverse` and slice assignment all mutate in place. - `copy.copy` and `copy.deepcopy` rebuild the subclass, including its own attributes, because they go through the pickling protocol rather than through `list.copy`. Anything that produces a *new* sequence loses it: slicing, `+`, `*`, `sorted()`, `list(s)`, and comprehensions over `s`. ## Fix one: override the result-producing methods ```python class Stack(list): def __getitem__(self, index): result = super().__getitem__(index) return type(self)(result) if isinstance(index, slice) else result ``` That is one method of five, and it only works if `type(self)` accepts a single iterable. If your subclass takes extra constructor arguments you also have to decide what the slice should carry over — which is the moment most people notice they wanted composition, not inheritance. ## Fix two: `collections.UserList` `collections.UserList` is a pure-Python sequence that keeps the real list in an ordinary attribute named `data`. Because every method is written in Python, it can and does use `self.__class__`: ```python from collections import UserList class Stack(UserList): def push(self, item): self.append(item) s = Stack([1, 2, 3]) type(s[0:2]) # <class '__main__.Stack'> type(s + [4]) # <class '__main__.Stack'> ``` The price is speed — every operation is a Python-level call that forwards to the wrapped list — and one constructor constraint: `self.__class__(...)` is called with a single iterable, so a subclass whose `__init__` demands extra required arguments will blow up on the first slice. ## Which to reach for If the subclass only *adds* attributes and helper methods, and nothing downstream depends on the type after a slice, plain `list` is fine and fast. If the type identity matters — because callers do `isinstance` checks or rely on overridden behaviour of the result — use `collections.UserList`, or stop inheriting and hold a list as a private attribute, exposing exactly the operations your object supports. The last option is usually the one that survives review, because it also avoids inheriting fifteen mutating methods you never intended to offer. ## The interview point The answer an interviewer is listening for is *who constructs the result object*. Once you say "the C implementation builds a concrete `list` and never looks at `type(self)`", the fixes follow on their own, and the follow-up about `collections.UserList` is a short step rather than a memorised fact. ## The cost of the wrapper, and one interop trap `collections.UserList` is not free on either axis. Every operation is a Python-level call that forwards to the wrapped list, so a tight `append` loop runs roughly three times slower than on a built-in list. More surprising in practice: a `UserList` **is not a list**. `isinstance(obj, list)` is false, so any code that special-cases the built-in takes a different path or refuses the object outright — `json.dumps(UserList([1, 2]))` raises `TypeError: Object of type UserList is not JSON serializable`, and library code with a fast path for exact lists will fall back or fail. That is the trade in one line: subclassing `list` keeps interop and speed but loses the type on every derived result; subclassing `collections.UserList` keeps the type but loses `isinstance(obj, list)`. Pick according to which of the two your callers actually depend on, and if the answer is "neither, really", hold a list as a private attribute and expose the handful of operations your object is meant to support.
- Which operations on a `list` subclass do preserve the subclass type?The in-place ones. `+=` uses `list.__iadd__`, which extends the existing object and returns it, and `append`, `extend`, `insert`, `sort`, `reverse` and slice assignment all mutate rather than build. `copy.copy` and `copy.deepcopy` also give you back the subclass, with its own attributes intact, because they go through the pickling protocol rather than through `list.copy`.
- What exactly would you have to override to make every result keep your subclass?`__getitem__` for the slice case, `__add__` and `__radd__`, `__mul__` and `__rmul__`, and `copy` — each rebuilding `type(self)(result)`. Your constructor then has to accept a single iterable, or you need to decide what extra state the derived object inherits. Enumerating that list is the standard argument for `collections.UserList` or for composition.
- Does `isinstance(s, list)` still hold, and does `sorted(s)` return the subclass?`isinstance` holds — a `list` subclass really is a list, which is why passing it to library code works. `sorted(s)` returns a plain `list`, because it builds a new list from the iterable rather than copying the argument's type. Same for `list(s)` and for any comprehension over `s`.
saying these in an interview costs you the question
- Claims slicing returns the subclass because Python is fully object-oriented
- Thinks overriding __init__ alone makes slices keep the subclass
- Believes += on a list subclass produces a new plain list
- Assumes sorted() gives back the subclass type
- Dismisses collections.UserList as dead Python 2 legacy