2.1.2 · D5OOP Fundamentals

Question bank — `__init__` constructor — initializing attributes

1,604 words7 min readBack to topic

Before the traps, one anchor picture we lean on repeatedly:


True or false — justify

TF1. __init__ creates the object.
False. __new__ allocates the blank shell; __init__ only fills an already-created object. __init__'s self is the shell it receives, not one it makes.
TF2. A class with no __init__ method cannot be instantiated.
False. If you omit __init__, Python uses the inherited object.__init__, which accepts self and no extra args — so Thing() still works, just with no custom attributes set.
TF3. self is a Python keyword.
False. self is just a conventional parameter name — the first argument is passed automatically no matter what you call it. Calling it this works, but breaks the convention every reader expects.
TF4. __init__ may return the finished object so the caller receives it.
False. __init__ must return ==None==; returning anything else raises TypeError. It is type.__call__ that returns the object, not __init__.
TF5. Two objects of the same class always share the attributes set in __init__.
False. Each call to Cls(...) gets its own self, so self.x = ... writes to that instance only. Sharing happens only for class attributes defined in the class body, not for instance attributes.
TF6. def __init__(self, x=[]) gives every new object its own fresh empty list.
False. The [] is built once when the function is defined and reused as the default across all instances — see Mutable default argument pitfall. Mutations leak between objects; use x=None then assign [] inside.
TF7. self.name = name and name = name do the same thing.
False. self.name = name stores the value on the object (persists after __init__ ends); name = name merely reassigns a local variable that vanishes when __init__ returns.
TF8. If you write both __new__ and __init__, only one of them runs.
False. Both run: __new__ first (returns the shell), then __init__ on that shell — provided __new__ returns an instance of the class. If it returns something else, __init__ is skipped. See __new__ vs __init__.
TF9. __init__ can take any number of parameters.
True. Beyond the mandatory self, you can add positional, keyword, default, *args, and **kwargs parameters freely — whatever the object needs at build time.
TF10. Validation belongs after creating the object, not inside __init__.
False. __init__ is the single gateway every object passes through, so validating there guarantees no invalid object can exist. Validating afterward allows a broken object to briefly live.

Spot the error

SE1. def __init__(name, age): self.name = name — what's wrong?
The first parameter must be self. Here name receives the object and age receives what you thought was name; self is undefined, so self.name raises NameError.
SE2. p = Person.__init__("Asha", 30) — what's wrong?
You called __init__ directly, so "Asha" becomes self and it returns None. Call the class: p = Person("Asha", 30), which runs __new__ then __init__ and returns the object.
SE3. Inside __init__: return self.name = name — what's wrong?
Two errors: assignment isn't an expression you can return, and __init__ must return None anyway. Just write self.name = name on its own line.
SE4. def __init__(self): self.total = self.total + 1 — what's wrong?
self.total is read before it's ever assigned, so it raises AttributeError. Inside __init__ the shell starts empty; you must assign an attribute before reading it.
SE5. def __init__(self, items=[]): self.items = items; self.items.append(0) — why does the list grow across objects?
The default [] is one shared list; each object appends 0 to the same list, so instance 3 sees [0, 0, 0]. Fix with items=None then self.items = items if items is not None else [].
SE6. class C: def __init__(): pass — why does C() fail?
__init__ declares zero parameters, but Python still passes self automatically, giving "takes 0 positional arguments but 1 was given". Add self: def __init__(self):.
SE7. def __init__(self, x): x = x — the attribute never appears. Why?
There's no self. on the left, so you reassign the local x to itself and lose it when __init__ returns. Write self.x = x.
SE8. class A: def __init__(self, n): self.n = n then A() — why the error?
n has no default, so it's required; A() supplies nothing for it, raising "missing 1 required positional argument". Either pass A(5) or give a default n=0.

Why questions

WY1. Why does Python pass self automatically instead of making you write it?
Cls(...) routes through type.__call__, which already holds the freshly-made object and inserts it as the first argument. Forcing you to pass it would be redundant and error-prone.
WY2. Why compute self.area = 3.14159 * r**2 in __init__ rather than every access?
If the value never changes, computing once at build time is cheaper than recomputing on every read. If the underlying data can change later, use a property instead so the value stays correct.
WY3. Why is balance=0 a safe default but items=[] dangerous?
0 is immutable — the shared default can never be mutated, so sharing is invisible. A list is mutable; mutating the one shared default corrupts every object that relied on it.
WY4. Why must __init__ return None?
type.__call__ already knows to return the object it built; if __init__ returned something else there'd be an ambiguity about what Cls(...) yields, so Python forbids it with TypeError.
WY5. Why put raise ValueError(...) inside __init__ for out-of-range input?
Because __init__ is the one gateway every object passes through — raising there means an invalid object is never even fully constructed, keeping the class's invariants guaranteed.
WY6. Why does self.x = x "copy" the value into the object even though both are called x?
They live in different namespaces: self.x is an attribute on the instance, x is a local parameter. The assignment binds the object's attribute to the value the parameter currently holds, and that binding outlives the local.
WY7. Why is __init__ called a "dunder" method?
"Dunder" = double underscore. The leading/trailing __ signals that Python (not you) calls it at defined moments — here, automatically after object creation. See Dunder methods.

Edge cases

EC1. What happens if __new__ returns an object of a different class than requested?
Then __init__ is not called — Python only runs __init__ when __new__ returns an instance of the class being constructed. See __new__ vs __init__.
EC2. Can __init__ set an attribute whose name matches a method of the class?
Yes, and it will shadow the method for that instance — self.run = 5 makes obj.run the integer, hiding any run() method. Legal but a bug magnet; avoid the name clash.
EC3. What does self.__class__ refer to inside __init__?
The actual class of the object being built — useful, but note that under inheritance it may be a subclass, not the class where __init__ was written. See self and instance attributes.
EC4. If a subclass defines its own __init__, does the parent's run automatically?
No. The subclass's __init__ fully overrides the parent's; you must call super().__init__(...) yourself if you want the parent's setup to run too.
EC5. What if you assign the same attribute twice in __init__, e.g. self.x = 1 then self.x = 2?
Legal — the last assignment wins, so self.x ends as 2. Each assignment just rebinds the attribute; there's no error for reassigning.
EC6. Can an object exist with zero attributes after __init__?
Yes. If __init__ sets nothing (or is the default object.__init__), the shell stays attribute-free until something adds one later. A valid object need not carry data.
EC7. What happens if __init__ raises an exception halfway through setting attributes?
The exception propagates and Cls(...) returns nothing; the partially-filled object is discarded (unreferenced) and eventually garbage-collected. The caller never receives a half-built object — which is exactly why validation belongs here.

Connections

  • OOP Fundamentals — the umbrella these traps live under.
  • self and instance attributes — the namespace distinction behind SE7, WY6, EC3.
  • __new__ vs __init__ — the two-stage build behind TF1, TF8, EC1.
  • Class vs Instance Attributes — sharing vs per-instance behind TF5.
  • Properties and computed attributes — the alternative in WY2.
  • Mutable default argument pitfall — the shared-default trap in TF6, SE5, WY3.
  • Dunder methods — why __init__ is auto-called (WY7).