Skip to contentSkip to navigation
MODULE 09 · LESSON 02

Constructors & the self Binding

Initialize instance state with __init__, distinguish class from instance attributes, and avoid mutable traps.

Core Learning Milestones:
__new__ vs __init__
Allocation vs configuration
self Is Desugared
instance.method(a) == Class.method(instance, a)
__init__ Returns None
Anything else raises TypeError
Defensive Construction
Guard clauses in __init__
Estimated Time
30 Minutes
Comprehensive deep dive
Difficulty Level
Beginner
Builds on Classes & Objects
Knowledge Check
15 Questions
+35 XP Potential
01 / Foundations

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.

Treating self as boilerplate
def deposit(amount): ...
# TypeError: missing self
self as the real instance
def deposit(self, amount):
  self.balance += amount
Think of it like this:

__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.

Full Contract in Action

Tracing __new__ and __init__ Execution Flow

Run the snippet below and watch the exact allocation-then-configuration order:

init_flow.py — Press 'Run Code' to execute in your browser
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)
Constructor Glossary: Core Concepts
__new__Allocator

Allocates memory and returns the raw instance. Rarely overridden except for immutable types or metaclasses.

__init__Configurator

Runs right after __new__, sets up instance attributes. Must always return None.

selfInstance ref

The instance itself, passed automatically as the first argument to every bound method call.

id(obj)Identity

Returns the object's CPython memory address — a stable per-object identity value.

02 / Construction Lifecycle Tracer

Step Through Object Construction

Click through each stage of what happens when you call Entity('goblin'):

1. __new__ allocates
obj = Entity.__new__(Entity) # raw, empty instance in memory

__new__ is the actual constructor — it allocates memory for the new object and returns it. This step happens before __init__ is ever called.

03 / Bound vs Unbound Calls

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.

04 / Hands-on Examples

Worked Hands-on Walkthroughs

Progress step-by-step from the raw new/init flow through bound calls to defensive constructor validation:

Walkthrough 01 · FoundationLevel 1 · Foundation

Tracing __new__ and __init__ Execution Flow

Lifecycle
Problem Scenario

__new__ creates the object in memory and passes it forward as 'self' to __init__.

Executable Solution
Tracing __new__ and __init__ Execution Flow
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)
Step-by-Step Explanation

__new__ creates the object in memory and passes it forward as 'self' to __init__.

Walkthrough 02 · Building UpLevel 2 · Applied

Comparing Bound Method Calls with Unbound Class Calls

Binding
Problem Scenario

w.execute('backup') is pure syntactic sugar for Worker.execute(w, 'backup').

Executable Solution
Comparing Bound Method Calls with Unbound Class Calls
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)
Step-by-Step Explanation

w.execute('backup') is pure syntactic sugar for Worker.execute(w, 'backup').

Walkthrough 03 · MasteryLevel 3 · Architecture

Defensive Invariant Validation inside __init__

Validation
Problem Scenario

Guarding inside __init__ guarantees that no BankAccount instance can exist with an empty ID or negative starting balance.

Executable Solution
Defensive Invariant Validation inside __init__
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)
Step-by-Step Explanation

Guarding inside __init__ guarantees that no BankAccount instance can exist with an empty ID or negative starting balance.

05 / Traps to Avoid

Common Traps & How to Avoid Them

Study the mistakes and clean fixes below before writing your own constructors:

!Trap 1: Syntax or Name Typos in Constructors (__init__) & Self
Common Trap
The Bug (Avoid)Buggy Code
# Mistyped variable or missing closing parenthesis result = constructors____init______self_val
What Happens:
NameError: name is not defined
The Clean FixCorrected Code
# Declare and verify variables before execution val = 10 print("Defined correctly:", val)
Recommended Practice:
Validate constructor arguments with explicit guard clauses before assigning them to self.
Why It Happens

Beginners frequently encounter typos or forgotten definitions before using an identifier.

!Trap 2: Unintended State Mutation or Scope Leakage
Subtle Trap
The Bug (Avoid)Buggy Code
# Modifying shared state or unexpected variable shadowing def update_state(data=[]): data.append(1) return data
What Happens:
Default mutable argument accumulates values across separate function calls.
The Clean FixCorrected Code
def update_state(data=None): if data is None: data = [] data.append(1) return data
Recommended Practice:
Validate constructor arguments with explicit guard clauses before assigning them to self.
Why It Happens

Default arguments in Python are evaluated once at function definition time, not at invocation time.

!Trap 3: Resource Leaks & High-Scale Performance Regressions
Architecture Trap
The Bug (Avoid)Buggy Code
# Opening resources in a tight loop without closing or bounding memory files = [open(f"log_{i}.txt", "w") for i in range(5000)]
What Happens:
OSError: [Errno 24] Too many open files (file descriptor exhaustion)
The Clean FixCorrected Code
for i in range(5): with open(f"log_{i}.txt", "w") as f: f.write("Safe")
Recommended Practice:
Validate constructor arguments with explicit guard clauses before assigning them to self.
Why It Happens

Failing to release system resources leads to process termination under heavy production workloads.

06 / Real-World Systems

Real-World Systems in Action

See how scripts, backend services, and enterprise runtimes rely on correct construction:

Developer ProductivityFoundation Script

Local Developer Automation Tool

SYSTEM #1
Organization Context

Individual developers and IT administrators automate everyday repetitive workflows.

Why This Pattern Works Here

A clean standalone script providing fast, local automation with zero dependencies.

Production Code Wrapper
Local Developer Automation Tool
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)
Cloud MicroservicesScaled Service · Applied

Backend API Service Integration

SYSTEM #2
Organization Context

SaaS platforms run backend microservices processing requests concurrently.

Why This Pattern Works Here

A modular service handler integrating validation and structured responses.

Production Code Wrapper
Backend API Service Integration
# Service integration layer
def handle_request(payload: dict) -> dict:
    return {"status": "success", "processed": True}
Distributed SystemsEnterprise · Architecture

High-Throughput Cloud Architecture

SYSTEM #3
Organization Context

Object creation in CPython allocates a PyObject struct on the heap, initializes its dictionary (__dict__), and binds self methods.

Why This Pattern Works Here

Object creation in CPython allocates a PyObject struct on the heap, initializes its dictionary (__dict__), and binds self methods.

Production Code Wrapper
High-Throughput Cloud Architecture
# High-resilience production pattern
import time

def process_stream(events: list) -> int:
    processed = 0
    for event in events:
        if event:
            processed += 1
    return processed
Key Takeaways

Key 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.
Study Guide & References

Frequently Asked Questions & Reference Hub

FAQ & References
Growth Mindset: self Clicks Once You Desugar It Yourself

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.

Related Built-in References Right Here
PyLabs Library Docs
Ready for Action

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.

Hands-on Code Challenge

Write a constructor that validates its own inputs before assigning them.

+40 XP Reward

Earn experience points toward your developer rank as soon as you pass.

~3 Minutes to Complete

Bite-sized knowledge check with instant explanations for every question.

Worked example

Initialize instance state cleanly inside __init__.

Example
class Item:
    def __init__(self, name): self.name = name
print(Item("Book").name)

How it works, step by step

  1. self.name sets instance state.

Your notes

Sign in to keep private notes alongside this lesson.

Sign in to take notes

Constructors & the self Binding practice

Sign-in required

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

Locked

You must be signed in to take this quiz. Sign in to test your understanding, track your answers, and earn XP.

Lesson incomplete

Not completed

Create an ID or sign in to save lesson completion across devices.

Create ID to save
Module lesson progress0/6 complete
Module cheat sheet