Why must you override `__new__`, not `__init__`, to subclass `int`, `str` or `tuple`?
answer
- The value is decided before setup runs
- Nothing left to assign afterwards
- The payload lives in the object itself
- Pass the value to the creating step
- return super().__new__(cls, value)
basics
~20 sThose built-ins are immutable, so their value is baked in while the object is being created. It has to be passed to int.new, str.new or tuple.new; by the time init runs the value exists and cannot be changed.
solid answer
~40 sAn immutable object's payload is part of the object itself, fixed at creation, so the only place that can influence it is the step that creates it. The subclass therefore overrides `__new__`, transforms or validates the incoming value, and delegates with `return super().__new__(cls, value)` — `int.__new__`, `str.__new__` and `tuple.__new__` each accept the value the instance will hold. `__init__` still runs afterwards with the same arguments, but `self.value = ...` there would only add an attribute; it cannot alter what the object *is*. Extra per-instance data can still be attached in `__new__`, since a Python-level subclass of an immutable gets a `__dict__` unless you define `__slots__ = ()`. Remember that inherited operators still return the base type: `PositiveInt(7) + 1` is a plain `int`.
code
python · 9 linesclass PositiveInt(int):
def __new__(cls, value):
if value <= 0:
raise ValueError("must be positive")
return super().__new__(cls, value)
n = PositiveInt(7)
print(n + 1, type(n + 1).__name__, isinstance(n, int))go deeper
Recall the rule and the reason in one line: these types are immutable, their value is fixed when the object is made, so the subclass must take part in making it rather than configuring it afterwards.
Write the pattern correctly from memory — validate the argument, then return super().__new__(cls, value) — and explain why an assignment in __init__ adds an attribute instead of changing the value.
Show that you have felt the consequences: inherited operators returning the base type, unpickling that needs __getnewargs__, and the honest comparison against wrapping the value in a small class instead of subclassing at all.
Own the guidance for when a domain value should be a built-in versus merely hold one, weighing interoperability with every API that takes an int or str against the maintenance cost of an inherited surface you cannot fully control.
Immutability in CPython is not a promise the class makes at the Python level; it is a property of how the object is laid out. An `int` stores its digits, a `str` its characters, a `tuple` its slot array — all inside the object, all written while it is being created and never afterwards. There is no assignable attribute holding the value, so there is nothing an initializer could set. That single fact decides which construction hook a subclass has to use. Recall the protocol. Calling a class runs `type.__call__`, which calls `__new__` to obtain the object and then `__init__` on the result. For an immutable built-in, all of the value-determining work happens in the first step: `int.__new__(cls, value)`, `str.__new__(cls, value)` and `tuple.__new__(cls, iterable)` each build an instance of `cls` already holding what it will hold forever. By the time `__init__` receives `self`, the object is finished. Writing `self.value = value` there does not change the integer; it merely bolts an unrelated attribute onto it, and the object still compares and behaves as whatever `__new__` produced. This is why `class Money(int): def __init__(self, cents): ...` appears to do nothing at all — because it does nothing at all. **The shape that works.** Override `__new__`, do the validation or transformation on the incoming argument, and delegate: ```python class PositiveInt(int): def __new__(cls, value): if value <= 0: raise ValueError("must be positive") return super().__new__(cls, value) ``` Two details are load-bearing. Pass `cls`, not the hard-coded base class, so a further subclass still produces its own type. And pass the value *to the base* `__new__`, not to `object.__new__` — the delegation goes through the immutable type's own creator, which is what accepts a payload argument at all. **Attaching extra data.** A Python-level subclass of an immutable built-in gets an instance `__dict__`, so `__new__` may set attributes on the object before returning it: ```python class Sql(str): def __new__(cls, text, *, source): obj = super().__new__(cls, text.strip()) obj.source = source return obj ``` The instance is still an immutable string as far as string operations are concerned; the added attribute lives beside the value rather than in it. Defining `__slots__ = ()` removes that `__dict__` again when you want the memory back and have no extra data to carry. **Argument compatibility.** Both steps receive the call's arguments. In the two classes above, `__init__` is inherited and is really `object.__init__`; because `__new__` *is* overridden, the extra arguments reaching it are tolerated and ignored. The moment you also define `__init__`, its signature must accept the same call — a mismatch shows up as a `TypeError` on an argument the initializer never expected. If you override `__init__` on such a subclass at all, it is for side effects only. **Behaviour you inherit and probably do not want.** Operators on the base type build base-type results: `PositiveInt(7) + 1` evaluates to a plain `int`, `Sql("a") + "b"` to a plain `str`, slicing a `tuple` subclass to a plain `tuple`. Keeping the subclass through arithmetic means overriding the operator methods to re-wrap results, which is a substantial amount of surface to maintain and a good moment to ask whether composition — a small class holding the value as an attribute — is the better design. Subclassing an immutable earns its keep mainly when the value must remain usable everywhere the base type is: as a dict key, in a format string, in a comparison. **Serialization and copying.** Because these objects are rebuilt by creation rather than by mutation, machinery that reconstructs them calls `cls.__new__(cls)` without your arguments. A `tuple` subclass whose `__new__` requires parameters therefore fails to unpickle with a missing-argument `TypeError` until it defines `__getnewargs__`, which tells the protocol which arguments to pass back into `__new__`. This is exactly why `collections.namedtuple` generates both a `__new__` and a `__getnewargs__` for the classes it builds. The same reconstruction path is used by shallow and deep copying, so the fix covers both. **A quick way to check yourself.** If you can name where the object's value physically lives, you can predict which hook is needed without memorizing a rule. A mutable object keeps its state in attributes that exist independently of the object's creation, so an initializer can still reach them. An immutable built-in keeps its state inside the object's own storage, written once at creation and never re-opened, so nothing that runs later has a way in. The same reasoning explains why `frozenset` and `bytes` subclasses follow the identical pattern, and why a subclass of a mutable type does not need it. **The one-sentence answer.** Mutable state can be set after the fact, so `__init__` suffices; an immutable's value is decided at creation, and `__new__` is the only step that participates in creation — so for `int`, `str` and `tuple` subclasses, the value goes to `super().__new__`.
- Your `int` subclass returns a plain `int` from arithmetic. Why, and what would you do about it?Because the inherited operator methods build results of the base type — they know nothing about your subclass. Keeping the type means overriding the arithmetic methods to re-wrap each result, which is a lot of surface for a little convenience. Often the better answer is composition: a small class holding the number as an attribute, exposing only the operations that make sense for it, so no unwanted behaviour is inherited at all.
- A `tuple` subclass with a custom `__new__` fails to unpickle. What is missing?`__getnewargs__`. Reconstruction does not call the class; it calls `cls.__new__(cls)` and passes the arguments the object reports through `__getnewargs__`. With none defined, a `__new__` that requires parameters raises a missing-argument `TypeError`. Defining `__getnewargs__` to return those values fixes both unpickling and copying, and is exactly why `collections.namedtuple` generates one for every class it builds.
- Can a subclass of `str` still hold extra per-instance data?Yes. A Python-level subclass of an immutable built-in gets an instance `__dict__`, so `__new__` can set attributes on the object before returning it. The string value itself stays immutable; the extra data lives beside it. Declaring `__slots__ = ()` removes that `__dict__` again when the subclass carries no extra data and you want the per-instance memory back.
It is the difference between painting a car and casting a bell. You can paint a finished car, but a bell's tone is set the instant the metal is poured — get it into the mould or not at all.
saying these in an interview costs you the question
- Sets the value with self.value in __init__
- Claims int and str cannot be subclassed at all
- Thinks super().__init__(value) can change an immutable
- Hard-codes the base class instead of passing cls
- Expects arithmetic on the subclass to preserve the subclass
- Passes the value to object.__new__ instead of the base type