Skip to content
C.W.K.
Stream
Lesson 01 of 07 · published

Defining a Class — class, __init__, self

~22 min · class, init, self, instance

Level 0Curious
0 XP0/93 lessons0/23 achievements
0/100 XP to next level100 XP to go0% complete

The class statement

A class statement creates a class object — itself a value, like everything in Python — and binds it to a name. Inside the body, you define methods (functions that take self as the first argument) and class-level attributes. Calling the class (Circle(3)) creates an instance.

__init__ — the post-construction setup, not the constructor

__init__ isn't technically the constructor. The actual constructor is __new__, which creates the bare instance. __init__ is the setup hook that runs immediately after, populating the new instance's attributes. You'll override __new__ only in unusual cases (singletons, immutable subclasses); __init__ is what you write 99% of the time.

self — the instance, by convention

The first argument of every instance method is self. Python passes the instance automatically when you call obj.method(...). The name self isn't a keyword — you could call it anything — but every Python developer expects self, and any other name will get the next reader to stop and frown. Don't fight it.

Calling methods — bound vs unbound

When you access obj.method, Python returns a bound method — a callable that's already linked to the instance. obj.method(x) works because self is filled in for you. Calling Circle.area(c) directly also works — that's the unbound (more accurately: function-on-class) form, where you pass the instance explicitly. The bound form is what you write daily.

Principle: A class is a blueprint — instances are the things made from it. Class-level state lives on the class itself; instance-level state lives on each instance. Confusing these two creates the "why does my list keep growing across instances" class of bugs we'll cover in the next lesson.

self is not permission to turn every helper into object state. Keep the values whose invariants the instance must protect; leave pure transformations as functions or static helpers. An object's identity comes from rules it preserves over time, not from collecting fields.

Code

A minimal class·python
class Circle:
    def __init__(self, radius):
        self.radius = radius            # attribute set on the instance

    def area(self):
        return 3.14159 * self.radius ** 2

c = Circle(5)
print(c.radius)         # 5
print(c.area())         # 78.54

# self is the instance — Python fills it in for you
print(Circle.area(c))   # 78.54  (calling on the class, passing instance)
__init__ vs __new__·python
class Logger:
    def __new__(cls, *args, **kwargs):
        print("__new__: creating instance")
        instance = super().__new__(cls)     # actual construction
        return instance

    def __init__(self, name):
        print("__init__: setting up")
        self.name = name

l = Logger("main")
# __new__: creating instance
# __init__: setting up

# 99% of the time you only override __init__
Multiple instances — each with its own state·python
class Account:
    def __init__(self, owner, balance=0):
        self.owner = owner
        self.balance = balance

    def deposit(self, amount):
        self.balance += amount
        return self.balance

a = Account("alice")
b = Account("bob", 100)

a.deposit(50)
b.deposit(25)

print(a.owner, a.balance)        # alice 50
print(b.owner, b.balance)        # bob 125    — independent state
Bound methods — the magic explained·python
class Greeter:
    def __init__(self, name):
        self.name = name

    def hi(self):
        return f"hello {self.name}"

g = Greeter("pippa")

# obj.method is a BOUND method — it remembers the instance
bound = g.hi
print(bound)                # <bound method Greeter.hi of <__main__.Greeter object at ...>>
print(bound())              # 'hello pippa'

# Plain Greeter.hi is just a function on the class
print(Greeter.hi(g))        # 'hello pippa' — pass instance explicitly

External links

Exercise

Define a class BankAccount with __init__(self, owner, opening_balance=0). Add three methods: deposit(amount), withdraw(amount) (raise ValueError if it would go below zero), and transfer_to(other_account, amount) that uses withdraw and deposit on the appropriate accounts. Test by creating two accounts, transferring between them, and printing each balance. Also try a transfer that would overdraw and confirm the exception fires.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.