Constructors & the self Binding
Initialize instance state with __init__, distinguish class from instance attributes, and avoid mutable traps.
self Is Not Magic — It's Just the First Argument
When you write Entity('goblin'), Python actually does two things behind the scenes: it calls __new__ to allocate a raw instance, then calls __init__ on that instance to configure it. The self parameter you see in every method is simply that instance, passed in automatically.
instance.method(arg) and Class.method(instance, arg) do exactly the same thing — the dot-call syntax is just Python quietly filling in self for you.
# TypeError: missing self
self.balance += amount
__new__ is like a factory receiving an empty shipping box, and __init__ is the worker who packs it. The box (instance) already exists by the time the worker (init) touches it — the worker just fills in what goes inside.
Tracing __new__ and __init__ Execution Flow
Run the snippet below and watch the exact allocation-then-configuration order:
class Entity:
def __new__(cls, *args, **kwargs):
print("1. __new__ called: Allocating instance memory")
instance = super().__new__(cls)
return instance
def __init__(self, name):
print(f"2. __init__ called: Initializing {name}")
self.name = name
e = Entity("PlayerOne")
print("3. Construction complete. Object:", e.name)Allocates memory and returns the raw instance. Rarely overridden except for immutable types or metaclasses.
Runs right after __new__, sets up instance attributes. Must always return None.
The instance itself, passed automatically as the first argument to every bound method call.
Returns the object's CPython memory address — a stable per-object identity value.
Step Through Object Construction
Click through each stage of what happens when you call Entity('goblin'):
__new__ is the actual constructor — it allocates memory for the new object and returns it. This step happens before __init__ is ever called.
Same Call, Two Ways to Write It
Bound method calls are just syntactic sugar over the unbound class-level call — seeing both side by side makes self click.
Bound instance calls
w.execute('backup')Calling a method on an instance automatically binds self to that instance behind the scenes.
Unbound class calls
Worker.execute(w, 'backup')The exact same call, desugared — calling the method on the class requires passing the instance explicitly as the first argument.
Identity & typing
id(obj)Returns the object's memory address in CPython — a stable identity for the lifetime of the object.
type(self) / self.__class__Both identify the actual runtime class of self, which matters for correct behavior in subclasses.
Worked Hands-on Walkthroughs
Progress step-by-step from the raw new/init flow through bound calls to defensive constructor validation:
Tracing __new__ and __init__ Execution Flow
__new__ creates the object in memory and passes it forward as 'self' to __init__.
class Entity:
def __new__(cls, *args, **kwargs):
print("1. __new__ called: Allocating instance memory")
instance = super().__new__(cls)
return instance
def __init__(self, name):
print(f"2. __init__ called: Initializing {name}")
self.name = name
e = Entity("PlayerOne")
print("3. Construction complete. Object:", e.name)__new__ creates the object in memory and passes it forward as 'self' to __init__.
Comparing Bound Method Calls with Unbound Class Calls
w.execute('backup') is pure syntactic sugar for Worker.execute(w, 'backup').
class Worker:
def __init__(self, worker_id):
self.worker_id = worker_id
def execute(self, task_name):
return f"Worker #{self.worker_id} executed task '{task_name}'"
w = Worker(42)
# 1. Standard instance method call
res1 = w.execute("backup")
# 2. Desugared explicit class method call
res2 = Worker.execute(w, "backup")
print("Instance call:", res1)
print("Desugared call:", res2)
print("Are results identical?", res1 == res2)w.execute('backup') is pure syntactic sugar for Worker.execute(w, 'backup').
Defensive Invariant Validation inside __init__
Guarding inside __init__ guarantees that no BankAccount instance can exist with an empty ID or negative starting balance.
class BankAccount:
def __init__(self, account_id, initial_balance=0.0):
if not isinstance(account_id, str) or not account_id.strip():
raise ValueError("account_id must be a non-empty string")
if initial_balance < 0:
raise ValueError(f"initial_balance cannot be negative, got {initial_balance}")
self.account_id = account_id.strip()
self.balance = float(initial_balance)
# Valid instantiation
acc = BankAccount("ACC-9821", 250.50)
print(f"Account {acc.account_id} created with balance ${acc.balance:.2f}")
# Invalid instantiations fail fast!
try:
BankAccount("", 100)
except ValueError as err:
print("Guard 1 caught:", err)
try:
BankAccount("ACC-001", -50)
except ValueError as err:
print("Guard 2 caught:", err)Guarding inside __init__ guarantees that no BankAccount instance can exist with an empty ID or negative starting balance.
Common Traps & How to Avoid Them
Study the mistakes and clean fixes below before writing your own constructors:
# Mistyped variable or missing closing parenthesis
result = constructors____init______self_val# Declare and verify variables before execution
val = 10
print("Defined correctly:", val)Beginners frequently encounter typos or forgotten definitions before using an identifier.
# Modifying shared state or unexpected variable shadowing
def update_state(data=[]):
data.append(1)
return datadef update_state(data=None):
if data is None:
data = []
data.append(1)
return dataDefault arguments in Python are evaluated once at function definition time, not at invocation time.
# Opening resources in a tight loop without closing or bounding memory
files = [open(f"log_{i}.txt", "w") for i in range(5000)]for i in range(5):
with open(f"log_{i}.txt", "w") as f:
f.write("Safe")Failing to release system resources leads to process termination under heavy production workloads.
Real-World Systems in Action
See how scripts, backend services, and enterprise runtimes rely on correct construction:
Local Developer Automation Tool
Individual developers and IT administrators automate everyday repetitive workflows.
A clean standalone script providing fast, local automation with zero dependencies.
class User:
def __init__(self, username, email):
self.username = username # Unique to this instance
self.email = email
u = User("alex", "alex@example.com")
print("Username:", u.username)Backend API Service Integration
SaaS platforms run backend microservices processing requests concurrently.
A modular service handler integrating validation and structured responses.
# Service integration layer
def handle_request(payload: dict) -> dict:
return {"status": "success", "processed": True}High-Throughput Cloud Architecture
Object creation in CPython allocates a PyObject struct on the heap, initializes its dictionary (__dict__), and binds self methods.
Object creation in CPython allocates a PyObject struct on the heap, initializes its dictionary (__dict__), and binds self methods.
# High-resilience production pattern
import time
def process_stream(events: list) -> int:
processed = 0
for event in events:
if event:
processed += 1
return processedKey Points & Concept Review
- __new__ allocates the instance memory and passes it to __init__ as self.
- __init__ must always return None; returning any value causes a TypeError.
- self is not magic syntax: instance.method(arg) is desugared to Class.method(instance, arg).
- id(obj) returns the unique memory address of the instance in CPython.
- type(self) and self.__class__ dynamically identify the runtime class of the instance.
- Enforce validation guards inside __init__ to guarantee objects are never constructed in an invalid state.
Frequently Asked Questions & Reference Hub
self feels like hidden magic until you rewrite one call as Class.method(instance, arg) by hand — it's completely normal for this to take a few tries before it feels obvious.
See exactly how allocation and configuration are split.
Understand how instance.method(arg) becomes Class.method(instance, arg).
Learn what object identity really means in CPython.
See why returning a value from __init__ raises an error.
Learn defensive patterns for rejecting invalid construction.
Explore the two ways to introspect an instance's runtime class.
You've Mastered Constructors & self!
You understand allocation vs configuration, method binding, and defensive constructor validation. Now lock in your knowledge with hands-on practice.
Write a constructor that validates its own inputs before assigning them.
Earn experience points toward your developer rank as soon as you pass.
Bite-sized knowledge check with instant explanations for every question.
Worked example
Initialize instance state cleanly inside __init__.
class Item:
def __init__(self, name): self.name = name
print(Item("Book").name)
How it works, step by step
- self.name sets instance state.
Constructors & the self Binding practice
Create a class Counter with an __init__ that initializes self.count = 0. Add a method increment(self) that adds 1 to self.count. Instantiate c = Counter(), call increment() twice, and print: Final count: 2.
Coding Practice is Locked
Sign in to access the interactive Python editor, write and run code checks against automated tests, use hints, and earn XP.
Practice quiz
Check yourself
15 questions with explanations after submission, so you can correct course as you go.
Constructors & the self Binding quiz
LockedYou must be signed in to take this quiz. Sign in to test your understanding, track your answers, and earn XP.
Lesson incomplete
Not completedCreate an ID or sign in to save lesson completion across devices.