skip to content

Why does a `tuple` subclass with a nonempty `__slots__` fail at class-creation time?

level: seniorimportance: nice to knowfreq 12%

answer

  1. slots need somewhere fixed to live
  2. the size depends on the value
  3. the payload sits inline in the object
  4. int and bytes refuse it too, list does not
  5. empty __slots__ is allowed and kills __dict__

basics

~20 s

A 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 lines
python
try:
    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) False

go deeper

for a junior

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.

for a middle

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.

for a senior

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__.

for a principal

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__

context