2.1.2 · D5OOP Fundamentals
Question bank — `__init__` constructor — initializing attributes
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).