Defining __len__ does more than make len() work. Read the last two lines — nothing in this class mentions truthiness.
class Basket:
def __init__(self):
self._items = []
def __len__(self):
return len(self._items)
basket = Basket()
print(len(basket)) # -> 0
print(bool(basket)) # -> False nobody wrote __bool__
print('empty' if not basket else 'has things') # -> 'empty'
- Truthiness falls back to length
When a class has no __bool__, Python asks __len__ instead and calls the object falsy when the length is zero. That is why `if items:` works on lists, strings and dicts, and why one method here makes your Stack behave the same way. Without either, every object is truthy — including an empty one, which is a quiet source of bugs.
- The leading underscore is a message, not a lock
_items says 'this is the inside of the class, do not reach for it'. Python will not stop anyone, and that is deliberate — the convention is enough among people reading the code. What it buys you is the freedom to change the storage later without breaking callers who only used push and pop.
- Wrapping is worth it when it removes a way to be wrong
A list already has append and pop. The Stack adds a name for the concept, a clear error on the empty case, and no way to reach into the middle by index — which is exactly what distinguishes a stack from a list.
A list already does the work; the class decides what is allowed. Store one privately, then write the four methods that touch it. Think carefully about what __init__ does with the argument it is handed.
push is append, pop is list.pop(), peek is the last element by index. __len__ returns the list’s length, and that is all you need for the truthiness cases. Two of the cases are about the default argument and about copying.
Default items to None and build the list inside — never items=[] in the signature. Use list(items) rather than items, so the caller’s list is copied. Guard pop and peek with `if not self._items: raise IndexError(...)`.
class Stack: def __init__(self, items=____): self._items = ____(items) if items is not None else [] def push(self, item): self._items.____(item) def pop(self): if not self._items: raise ____(...) return self._items.pop() def peek(self): ... def ______(self): return len(self._items)
class Stack:
"""A last-in, first-out collection."""
def __init__(self, items=None):
self._items = list(items) if items is not None else []
def push(self, item):
self._items.append(item)
def pop(self):
if not self._items:
raise IndexError("pop from an empty stack")
return self._items.pop()
def peek(self):
if not self._items:
raise IndexError("peek at an empty stack")
return self._items[-1]
def __len__(self):
return len(self._items)
It copies, so the caller’s list and the stack’s storage are different objects and neither can surprise the other. And it accepts any iterable — a tuple, a generator, another Stack’s contents — rather than only a list. One call, two properties, and both are tested here.
items=[] in the signature would be evaluated once when the class body runs, so every Stack built without arguments would share one list — and your second stack would arrive with the first one’s contents. It is exactly the shopping-basket bug, and it is worth meeting twice because in a class it looks even more innocent.
An empty list already raises IndexError when you pop it, so callers who know Python already know what to catch, and `except IndexError` works whether they were handed a Stack or a list. Inventing StackEmptyError would be more precise and less useful — reach for a custom exception when callers genuinely need to tell your failure apart from the built-in one.
For most code, use a plain list — append and pop already are a stack, and everyone reading knows what they do. If you need a stack *and* a queue, collections.deque pops from either end in constant time, which a list cannot do at the front. Write the class when the type is part of your domain and the restriction is the point: when it matters that nobody can reach into the middle, or when push and pop should do something extra like record history.
- Add __iter__ so `for item in stack` walks from the top down, and decide whether iterating should consume the stack or leave it alone.
- Add __repr__ showing the contents top-first, then check what it does to an empty stack.
- Make a MaxStack that also answers .maximum() in constant time — the trick is a second stack, and it is a genuinely good interview question.