skip to content

questions

4

Why must a `tuple` subclass set its contents in `__new__` rather than in `__init__`?

level: middleimportance: must knowfreq 45%

answer

  1. immutable means fixed at birth
  2. which hook runs first, and what does it receive?
  3. cls, not self, and it must return
  4. TypeError before the body ever runs
  5. __getnewargs__ for copy and pickle

basics

~20 s

A tuple's items are fixed when the object is allocated, and allocation happens in __new__. __init__ runs afterwards and cannot change them, so a tuple subclass that defines only __init__ fails at construction with a TypeError from tuple.__new__.

solid answer

~50 s

`tuple` is immutable: its items are stored in the object at allocation time, and allocation is `__new__`, which runs first and receives the class rather than an instance. So `class Point(tuple)` with `def __init__(self, x, y)` breaks immediately — `Point(3, 4)` reaches `tuple.__new__` with two arguments and raises `TypeError: tuple expected at most 1 argument, got 2`, before your `__init__` body ever runs. The working form is `def __new__(cls, x, y): return super().__new__(cls, (x, y))`, with properties for named access. Two consequences follow. Slicing, `+` and `*` still return plain tuples, exactly as they do for a `list` subclass. And `copy.deepcopy` or `pickle` break, because they re-create the object by calling `__new__` with the original items, which no longer matches your two-argument signature — you have to define `__getnewargs__`. That accumulating checklist is why `typing.NamedTuple` is usually the better answer.

code

python · 9 lines
python
class Broken(tuple):
    def __init__(self, x, y):
        self.total = x + y


try:
    Broken(3, 4)
except TypeError as exc:
    print(exc)  # tuple expected at most 1 argument, got 2

go deeper

for a junior

Recall that construction has two steps and that immutable built-ins fix their contents in the first one. Recognising TypeError: tuple expected at most 1 argument as a missing __new__ is the takeaway.

for a middle

Write the subclass correctly on the spot: __new__(cls, ...) passing a single iterable to super().__new__, returning the instance, with properties for named access. Explain why __init__ cannot do the job.

for a senior

Demonstrate the failure modes that surface later — copy and pickle needing __getnewargs__, slicing degrading to plain tuple, equality with bare tuples — and justify when hand-rolling beats a generated record type.

for a principal

Own the choice of value-type vocabulary across a codebase: which immutable record shape teams reach for by default, what interop with sequence-consuming code costs, and when custom construction logic genuinely warrants a hand-written __new__.

## Allocation versus initialisation Building an object in Python is two steps. `__new__` is called first, with the class as its first argument; it allocates and returns the instance. `__init__` is called second, on the object `__new__` returned, and its job is to set attributes. For mutable types the split rarely matters — you allocate an empty object and fill it in `__init__`. A tuple cannot be filled in afterwards. Its item pointers are written into the object when it is allocated and are never rewritten, which is exactly what immutability means at the C level. There is no `self[0] = x` to write in `__init__`, and no private back door either. So the contents must be handed to `tuple.__new__`. ## The failure you actually see ```python class Broken(tuple): def __init__(self, x, y): self.total = x + y Broken(3, 4) # TypeError: tuple expected at most 1 argument, got 2 ``` The error is confusing until you trace the call. `Broken(3, 4)` calls the type, which calls `Broken.__new__(Broken, 3, 4)`. `Broken` does not define `__new__`, so this is `tuple.__new__`, which accepts a class and at most one iterable. Two positional arguments is one too many, and it raises before `__init__` is reached. The message mentions `tuple` rather than `Broken` because `tuple.__new__` is the code that raised. ## The correct shape ```python class Point(tuple): def __new__(cls, x, y): return super().__new__(cls, (x, y)) @property def x(self): return self[0] @property def y(self): return self[1] def __getnewargs__(self): return (self[0], self[1]) ``` Three details are load-bearing. `__new__` takes `cls`, not `self`, and must *return* the new object — forgetting the `return` gives you `None`. The items are passed as a single iterable, `(x, y)`, not as separate arguments. And `__getnewargs__` exists because `copy.deepcopy` and `pickle` reconstruct a tuple subclass by calling `__new__` with the items the tuple holds; without it, deep-copying a `Point` raises `TypeError: Point.__new__() missing 1 required positional argument: 'y'`. That is a genuinely nasty production bug, because it only fires the first time something copies or serialises the object. ## `__init__` is still called A common half-truth is that `__init__` never runs on an immutable subclass. It does run, as long as `__new__` returns an instance of the class, and it receives the same arguments `__new__` received. It simply cannot change the items. It is a fine place to compute and cache a derived attribute, since a plain tuple subclass instance still has a `__dict__`. ## What still does not work The subclass does not survive operations that build a new sequence. `p[:1]`, `p + (1,)` and `p * 2` all return plain tuples, for the same reason a `list` subclass loses its type: the C implementation constructs a concrete `tuple` and cannot know your constructor's signature. Equality and hashing also come from `tuple`, so a `Point(3, 4)` compares equal to the bare tuple `(3, 4)` unless you override `__eq__` — sometimes convenient, sometimes a bug in a set or a dictionary key. ## The better answer Most of the time the reason someone subclasses `tuple` is to get a small immutable record with named fields. `typing.NamedTuple` already is a correctly built tuple subclass: it generates `__new__`, supports defaults, keyword construction and `__getnewargs__`, declares an empty `__slots__` so instances carry no `__dict__`, and gives a readable `repr`. Saying "I would write it as a `typing.NamedTuple` and only hand-roll the subclass if I needed behaviour it cannot express" is the answer an interviewer is looking for. Hand-rolling is still justified when you need custom construction logic — validating or normalising arguments before allocation, accepting several call shapes, or interning instances — because those all live in `__new__`, which is the one hook a generated record type does not give you. ## The same rule for every immutable built-in Nothing here is special to `tuple`. `str`, `bytes`, `int`, `float` and `frozenset` are all immutable, all fix their value at allocation, and all need `__new__` in a subclass: ```python class Upper(str): def __new__(cls, value): return super().__new__(cls, value.upper()) Upper("hi") # 'HI' ``` And all of them share the second half of the story: `Upper("hi") + "x"` is a plain `str`, `MyInt(3) + 1` is a plain `int`. The built-in operations construct their result as the concrete base type, because they cannot know what arguments a subclass constructor requires. If you take one rule away from subclassing an immutable built-in, take that pair: **the value is fixed in `__new__`, and every derived value comes back as the base type.**

  • What breaks when you deep-copy or pickle a tuple subclass with a custom `__new__` signature?
    Both rebuild the object by calling `__new__` with the arguments `__getnewargs__` reports, defaulting to the tuple's own items as a single iterable. If `__new__` takes `(cls, x, y)`, that call fails with a TypeError about a missing positional argument. Defining `__getnewargs__` to return the arguments your `__new__` expects fixes copying and pickling together.
  • Does `__init__` run at all on a tuple subclass?
    Yes. Once `__new__` returns an instance of the class, `__init__` is called with the same arguments. It cannot alter the items, but it can set ordinary attributes, since a plain tuple subclass instance still has a `__dict__`. What it cannot do is stand in for `__new__`.
  • When would you choose a frozen dataclass over `typing.NamedTuple`?
    Choose `typing.NamedTuple` when the value should behave as a tuple — unpacking, indexing, comparing equal to a plain tuple, passing to code that expects a sequence. Choose a frozen `dataclasses.dataclass` when you want named immutable fields without sequence semantics, or when you need inheritance, per-field metadata, or equality that does not silently match a bare tuple.

saying these in an interview costs you the question

  • Says you can assign to self[0] inside __init__ of a tuple subclass
  • Claims __init__ never runs on an immutable subclass
  • Passes the items to tuple.__new__ as separate arguments
  • Forgets to return the instance from __new__
  • Expects slicing or + to keep the subclass type
  • Hand-rolls a tuple subclass where typing.NamedTuple fits

context

open as a page

Why does slicing a `list` subclass return a plain `list` instead of the subclass?

level: juniorimportance: should knowfreq 35%

basics

~20 s

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

open as a page

Why does a cached total on a `list` subclass go stale when only `append` is overridden?

level: seniorimportance: should knowfreq 30%

basics

~20 s

Built-in list mutators are C code that never calls your Python append: extend, +=, insert, slice assignment and sort change the contents behind the override, so the cache is never invalidated. Subclassing list gives you no single mutation hook.

open as a page

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

level: seniorimportance: nice to knowfreq 12%

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

open as a page