Why does a `tuple` subclass with a nonempty `__slots__` fail at class-creation time?
answer
- slots need somewhere fixed to live
- the size depends on the value
- the payload sits inline in the object
- int and bytes refuse it too, list does not
- empty __slots__ is allowed and kills __dict__
basics
~20 sA tuple stores its items inline in the object, so instances of different lengths have different sizes and a slot has no fixed offset to live at. CPython rejects the class immediately with TypeError: nonempty __slots__ not supported for subtype of 'tuple'.
solid answer
~50 s`__slots__` works by reserving a fixed offset inside every instance, which requires all instances of a class to be the same size. `tuple` is a variable-length built-in: its item pointers live inline at the end of the object, so a two-item tuple and a five-item tuple genuinely differ in size and there is no fixed place to put a slot. CPython therefore refuses the class as soon as the `class` statement executes, rather than failing on first instantiation. `int` and `bytes` behave the same way for the same reason. `__slots__ = ()` is allowed and is useful: it declares that the subclass adds no attributes, so instances carry no `__dict__` — that is exactly what `typing.NamedTuple` emits. A `list` subclass, by contrast, accepts nonempty slots, because a list's elements live in a separately allocated array while the list object itself is fixed-size.
code
python · 13 linestry:
class Point(tuple):
__slots__ = ("x", "y")
except TypeError as exc:
print(exc) # nonempty __slots__ not supported for subtype of 'tuple'
class Pair(tuple):
__slots__ = ()
p = Pair((1, 2))
print(p, hasattr(p, "__dict__")) # (1, 2) Falsego deeper
Know what __slots__ is for at all: it replaces the per-instance attribute dictionary with fixed storage. You are not expected to know which built-ins refuse it.
Be able to say that slots need a fixed offset in every instance, and that a tuple's items are stored inline so instance size varies. Recognise the error text as a class-creation failure.
Explain the layout reason rather than reciting the rule, contrast it with a list subclass, and know that __slots__ = () is both legal and worth writing because it removes the per-instance __dict__.
Treat it as a signal about value-type design: if a type needs slots to be affordable at scale, ask whether a tuple subclass is the right base at all, and what the standard immutable record shape should be across the codebase.
## What `__slots__` actually does By default every instance of a Python class carries a `__dict__` for its attributes. Declaring `__slots__ = ("x", "y")` replaces that dictionary with descriptors that read and write fixed offsets inside the instance itself. That is where the memory saving and the small speed win come from — and it is also the constraint: the offset must be known when the type is created, and it must be the same for every instance of the type. ## Why `tuple` cannot host one Most objects have a fixed instance size, recorded on the type. A handful of built-ins are *variable-length*: the payload is stored inline in the object, immediately after the header, and each instance's total size depends on how much payload it holds. At the C level these types declare a nonzero `tp_itemsize`. `tuple` is one — the item pointers are part of the object — and so are `int`, whose digits are stored inline, and `bytes`. For such a type there is no offset that is past the payload for every instance, because the payload length varies per instance. Rather than silently corrupt memory, CPython refuses: ```python class Point(tuple): __slots__ = ("x", "y") # TypeError: nonempty __slots__ not supported for subtype of 'tuple' ``` The error fires while the `class` body is being turned into a type, so it is a hard startup failure, not something that waits for the first instance. Note also what the reason is *not*: immutability. A tuple subclass instance normally has a perfectly ordinary `__dict__` and you can set attributes on it. The problem is layout, not mutability. ## `list` is not variable-length in this sense It is worth contrasting, because it is the fact that makes the rule click. A `list` object holds a pointer to a separately allocated array of items; the list object itself is a fixed-size header. So the following is legal, and the slot lives at a known offset: ```python class Tagged(list): __slots__ = ("tag",) ``` The same is true of most classes people write. The rejection is specific to subtyping a variable-length built-in. ## The empty tuple is not a no-op `__slots__ = ()` is accepted on a tuple subclass and does real work: it declares that the class adds no per-instance attributes, so instances are created without a `__dict__`. The effects are memory (no dictionary per value) and discipline (a typo in an attribute name raises `AttributeError` instead of silently creating a new attribute). That is why the standard library's tuple-based record types declare an empty `__slots__`, and why a `typing.NamedTuple` instance has no `__dict__`: ```python import typing class Point(typing.NamedTuple): x: int y: int hasattr(Point(1, 2), "__dict__") # False ``` If you hand-roll a tuple subclass and want the same property, write `__slots__ = ()` yourself; it is easy to forget, and forgetting it silently reintroduces a per-instance dictionary on a type whose whole point was to be small. ## Where the extra state goes instead When you genuinely need a value that behaves like a tuple *and* carries something the tuple does not, the options are ordinary design choices rather than tricks: - Make the extra data another field of the tuple, exposed through a property, so it participates in equality, hashing and unpacking like everything else. - Accept the per-instance `__dict__`: drop `__slots__`, set the attribute in `__init__`, and pay a dictionary per value. Remember that the attribute will not survive `copy.deepcopy` or pickling unless you arrange for it, since reconstruction goes through `__new__` and the tuple's items. - Stop subclassing `tuple`. A frozen `dataclasses.dataclass` accepts a nonempty `__slots__`, gives you named fields, and is the better home for a value type that is not really a sequence. ## Why an interviewer asks it This is a curiosity question, not a gate. It comes up when a candidate reaches for `__slots__` to shrink a hand-rolled tuple subclass and hits the error live. What it tests is whether you can reason about object layout from first principles — fixed offsets need fixed sizes — instead of memorising which built-ins are on a list. The strong answer explains the layout in one sentence, notes that the empty form is allowed and useful, and moves on.
- What does `__slots__ = ()` buy you on a tuple subclass, given it defines no slots?It suppresses the per-instance `__dict__`. Without it, every instance of a tuple subclass carries a dictionary it will probably never use, and a misspelled attribute silently becomes a new one. With it, instances are smaller and unknown attribute assignment raises `AttributeError`. The generated tuple-based record types in the standard library declare it for exactly this reason.
- Why is a `list` subclass allowed a nonempty `__slots__` when a `tuple` subclass is not?A list object is a fixed-size header holding a pointer to a separately allocated array of items, so every instance is the same size and a slot has a stable offset. A tuple stores its item pointers inline, so instance size varies with length. The rule follows the layout, not the mutability.
- You need a tuple-like value with one extra piece of state. What do you do?Usually make the extra state another field of the tuple and expose it through a property, so it takes part in equality, hashing and unpacking. If it must be out of band, accept the per-instance `__dict__` and arrange for it to survive reconstruction, since copy and pickle rebuild through `__new__`. If the value is not really a sequence, a frozen dataclass is the better base.
saying these in an interview costs you the question
- Says __slots__ is rejected because tuples are immutable
- Thinks an empty __slots__ tuple is a no-op
- Believes every built-in refuses a nonempty __slots__
- Expects the error on first instantiation, not at class creation
- Assumes a tuple subclass instance never has a __dict__