PythonClasses & Objects

Classes & Objects

A class is defined with the class keyword and acts as the blueprint for a new type of object. An object (or instance) is what you get when you call the class like a function — each call produces a separate, independent value with its own copy of whatever data the class defines. This page walks through the syntax for both, and spends extra time on self, the one piece of the puzzle that trips up almost everyone the first time they see it.

The `class` keyword and creating instances

A minimal class needs nothing more than the keyword, a name, and a body. Even an empty class can be instantiated — calling the class name with parentheses creates a new object.

Python
class Dog:
    pass

my_dog = Dog()
print(type(my_dog))
print(my_dog)
<class '__main__.Dog'>
<__main__.Dog object at 0x7f2c1a3b4d90>

my_dog is an object — a real, distinct value living in memory. The memory address will differ every time you run this, but the important part is that calling Dog() produced something new, not a reference to the class itself.

Setting instance attributes inside methods

A class on its own is just a container for methods (functions defined inside it). To give each object its own data, methods set instance attributes — values attached to that specific object — using self.attribute_name = value. The most common place to do this is inside __init__, which we'll cover in full on the next page, but any method can create or update instance attributes.

Python
class Dog:
    def set_name(self, name):
        self.name = name

my_dog = Dog()
my_dog.set_name("Rex")
print(my_dog.name)
Rex
Understanding `self`

self is the first parameter of every regular method in a class, and it refers to the specific object the method was called on. Python fills this parameter in automatically — you never pass it yourself. When you write my_dog.set_name("Rex"), Python translates that call behind the scenes into something equivalent to Dog.set_name(my_dog, "Rex"). The object you called the method on becomes self inside the method body.

What you write

What Python actually does

my_dog.set_name("Rex")

Calls set_name with self bound to my_dog and name bound to "Rex".

inside the method: self.name = name

Sets the name attribute on whichever object self currently refers to.

This is why self.name = name inside a method changes only the object the method was called on, and not every Dog in existence: self is that one specific object, not the class.

Forgetting self raises a TypeError
If you forget to include `self` as the first parameter of a method, Python still passes the calling object as the first argument — it just lands in whatever parameter you did declare, or there is no parameter to receive it at all. In the second case, calling the method raises a `TypeError` complaining about too many positional arguments, because Python is trying to pass the instance in and the method's signature has nowhere to put it.

Python
class Dog:
    def bark():  # missing self!
        print("Woof!")

my_dog = Dog()
my_dog.bark()
TypeError: Dog.bark() takes 0 positional arguments but 1 was given
Worked example: a BankAccount class

Let's put all of this together in a class that is actually useful: a simple bank account with a balance, a way to deposit money, a way to withdraw money that refuses to overdraw the account, and a way to check the current balance.

Python
class BankAccount:
    def __init__(self, owner, balance=0):
        self.owner = owner
        self.balance = balance

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

    def withdraw(self, amount):
        if amount > self.balance:
            print(f"Cannot withdraw {amount}: insufficient funds")
            return
        self.balance -= amount

    def get_balance(self):
        return self.balance

Now let's create two separate accounts and show that each one keeps its own, independent balance:

Python
alice_account = BankAccount("Alice", 100)
bob_account = BankAccount("Bob", 50)

alice_account.deposit(200)
bob_account.withdraw(20)

print(alice_account.get_balance())
print(bob_account.get_balance())

bob_account.withdraw(1000)
300
30
Cannot withdraw 1000: insufficient funds

Notice that depositing into alice_account had zero effect on bob_account.balance. BankAccount is the blueprint, defined exactly once in the code above. alice_account and bob_account are two separate objects built from it, each with its own owner and balance living independently in memory. That independence — one object's state never leaking into another's — is the entire point of instance attributes.

Class vs. object, side by side
  • The class (BankAccount) is the definition — written once, describing what every account will be able to do.

  • An object (alice_account, bob_account) is one instance created by calling BankAccount(...) — you can create as many as you want, each independent.

  • Methods (deposit, withdraw, get_balance) are defined once on the class but act on whichever object called them, via self.

  • Instance attributes (self.owner, self.balance) are created per object, so each instance has its own copy.

Why the guard in withdraw matters
Without the `if amount > self.balance` check, `withdraw` would happily subtract more than the account has, leaving `balance` negative. Guarding state-changing methods like this is a small preview of encapsulation — the class itself is responsible for keeping its own data valid, so callers don't have to remember the rule themselves.