Why must a `tuple` subclass set its contents in `__new__` rather than in `__init__`?
answer
- immutable means fixed at birth
- which hook runs first, and what does it receive?
- cls, not self, and it must return
- TypeError before the body ever runs
- __getnewargs__ for copy and pickle
basics
~20 sA 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 linesclass 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 2go deeper
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.
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.
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.
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