Skip to content

Classes and Objects

💡
Before you start

You need Python installed, and to know how to run a .py file. If python3 --version in a terminal prints a version number, you are ready. If it does not — or if you have never opened a terminal — do Getting Started with Python first; it installs Python and runs your first program, and takes about fifteen minutes. Nothing else is needed: no account, no payment, no extra software.

What is Object-Oriented Programming?

Object-Oriented Programming (OOP) organizes code around objects — bundles of data and behavior that model real-world things. A class is the blueprint; an object is an instance built from that blueprint.

Think of it like this: "Dog" is a class (the concept), while "Rex the golden retriever" is an object (a specific dog).

Creating Your First Class

class Dog:
    def __init__(self, name, breed):
        self.name = name        # instance attribute
        self.breed = breed      # instance attribute

    def bark(self):             # method
        print(f"{self.name} says: Woof!")

    def info(self):             # method
        print(f"{self.name} is a {self.breed}")

# Create objects (instances)
rex = Dog("Rex", "Golden Retriever")
luna = Dog("Luna", "German Shepherd")

rex.bark()       # Rex says: Woof!
luna.info()      # Luna is a German Shepherd
💡
What is self?

self refers to the specific object calling the method. When you write rex.bark(), Python passes rex as self automatically. Every method must have self as its first parameter.

The __init__ Constructor

__init__ runs automatically when you create a new object. Use it to set up the object's initial state:

class BankAccount:
    def __init__(self, owner, balance=0):
        self.owner = owner
        self.balance = balance
        self.transactions = []

    def deposit(self, amount):
        if amount > 0:
            self.balance += amount
            self.transactions.append(f"+${amount}")
            print(f"Deposited ${amount}. Balance: ${self.balance}")

    def withdraw(self, amount):
        if amount > self.balance:
            print("Insufficient funds!")
            return
        self.balance -= amount
        self.transactions.append(f"-${amount}")
        print(f"Withdrew ${amount}. Balance: ${self.balance}")

    def statement(self):
        print(f"\nAccount: {self.owner}")
        print(f"Balance: ${self.balance}")
        print(f"Transactions: {', '.join(self.transactions)}")

# Usage
account = BankAccount("Alice", 1000)
account.deposit(500)         # Deposited $500. Balance: $1500
account.withdraw(200)        # Withdrew $200. Balance: $1300
account.statement()

Instance vs Class Attributes

class Car:
    # Class attribute — shared by ALL instances
    wheels = 4

    def __init__(self, make, model, year):
        # Instance attributes — unique to each object
        self.make = make
        self.model = model
        self.year = year
        self.mileage = 0

    def drive(self, km):
        self.mileage += km

car1 = Car("Toyota", "Camry", 2024)
car2 = Car("Honda", "Civic", 2023)

print(car1.wheels)      # 4 (from class)
print(car2.wheels)      # 4 (from class)
print(Car.wheels)       # 4 (access via class)

car1.drive(100)
print(car1.mileage)     # 100
print(car2.mileage)     # 0 (each has its own)

Special Methods (Dunder Methods)

Python uses double-underscore methods (dunder methods) to define how objects behave with built-in operations:

class Product:
    def __init__(self, name, price):
        self.name = name
        self.price = price

    def __str__(self):
        """Called by print() and str()"""
        return f"{self.name}: ${self.price:.2f}"

    def __repr__(self):
        """Called in debugger and interactive shell"""
        return f"Product('{self.name}', {self.price})"

    def __eq__(self, other):
        """Called by == operator"""
        return self.name == other.name and self.price == other.price

    def __lt__(self, other):
        """Called by < operator (enables sorting)"""
        return self.price < other.price

items = [
    Product("Laptop", 999.99),
    Product("Mouse", 29.99),
    Product("Keyboard", 79.99)
]

print(items[0])              # Laptop: $999.99
items.sort()                 # Sorts by price (uses __lt__)
for item in items:
    print(item)              # Mouse, Keyboard, Laptop

Encapsulation

Encapsulation means hiding internal details and controlling access to data:

class User:
    def __init__(self, username, password):
        self.username = username
        self._password = password    # Convention: "private" (one underscore)

    def check_password(self, attempt):
        return attempt == self._password

    def change_password(self, old, new):
        if self.check_password(old):
            self._password = new
            print("Password changed!")
        else:
            print("Wrong password!")

user = User("alice", "secret123")
user.check_password("secret123")     # True
user.change_password("secret123", "newpass456")
💡
Python's privacy convention

A single underscore (_name) signals "don't access this directly." A double underscore (__name) triggers name mangling for stronger protection. Neither truly prevents access — Python trusts developers.

Practical Example: Task Manager

class Task:
    def __init__(self, title, priority="medium"):
        self.title = title
        self.priority = priority
        self.completed = False

    def complete(self):
        self.completed = True

    def __str__(self):
        status = "done" if self.completed else "pending"
        return f"[{status}] {self.title} ({self.priority})"

class TaskManager:
    def __init__(self):
        self.tasks = []

    def add(self, title, priority="medium"):
        task = Task(title, priority)
        self.tasks.append(task)
        print(f"Added: {title}")

    def complete(self, title):
        for task in self.tasks:
            if task.title == title:
                task.complete()
                print(f"Completed: {title}")
                return
        print(f"Task not found: {title}")

    def show(self, show_completed=True):
        for task in self.tasks:
            if show_completed or not task.completed:
                print(f"  {task}")

    def pending_count(self):
        return sum(1 for t in self.tasks if not t.completed)

# Usage
mgr = TaskManager()
mgr.add("Write report", "high")
mgr.add("Buy groceries", "low")
mgr.add("Fix bug #42", "high")
mgr.complete("Write report")

print(f"\nAll tasks:")
mgr.show()
print(f"\nPending: {mgr.pending_count()}")

Now Do It Yourself: Five Steps

A class is a template for making things that carry their own data and know how to do their own jobs. You will define one, make two of them, and meet the two errors every beginner hits. Every output below is exactly what Python printed.

1
Define a class and make one

Go: open a terminal, run cd ~, and create a file called dogs.py.

Do: type these six lines and run python3 dogs.py.

class Dog:
    def __init__(self, name):
        self.name = name

d = Dog("Rex")
print(d.name)

You should see: Rex. class Dog is the template; Dog("Rex") makes one actual dog. __init__ runs automatically at that moment and its job is to store what makes this dog different from any other.

If not: TypeError: Dog.__init__() missing 1 required positional argument: 'name' means you wrote Dog() with nothing inside. __init__ asks for a name, so you must supply one.

2
Understand what self actually is

Go: same file, add to it.

Do: make the file read exactly this and run it.

class Dog:
    def __init__(self, name):
        self.name = name
    def speak(self):
        return self.name + " says woof"

d = Dog("Rex")
print(d.speak())

You should see: Rex says woof. self is the particular dog the method was called on. You never pass it — writing d.speak() hands d in as self automatically. That is the whole trick.

If not: TypeError: Dog.speak() takes 0 positional arguments but 1 was given means you wrote def speak(): without self. The message reads backwards until you know what it means: Python passed the object in, and your method had nowhere to put it. Every method needs self as its first argument.

3
Make two, and see that they are separate

Go: same file, change only the bottom.

Do: replace the last two lines with these four and run it.

rex = Dog("Rex")
bella = Dog("Bella")
print(rex.speak())
print(bella.speak())

You should see: Rex says woof then Bella says woof. One class, two independent objects, each carrying its own name. Changing rex.name would not touch bella at all.

If not: if both print the same name, you assigned to Dog.name instead of self.name somewhere — that puts one value on the class itself and every object shares it. Anything that belongs to one object goes on self.

4
Store more than one thing, and let a method use it

Go: same file, replace the contents.

Do: type these eight lines and run it.

class Dog:
    def __init__(self, name, age):
        self.name = name
        self.age = age
    def describe(self):
        return f"{self.name} is {self.age} years old"

print(Dog("Rex", 3).describe())

You should see: Rex is 3 years old. Two pieces of data on one object, and a method that reads both. Note the object was never stored in a variable — it was made, used, and discarded on one line, which is perfectly normal.

If not: AttributeError: 'Dog' object has no attribute 'age' means __init__ accepted age but never stored it with self.age = age. Accepting an argument and keeping it are two separate acts.

5
Change an object after it exists

Go: same file, replace the bottom line.

Do: use these four lines instead and run it.

rex = Dog("Rex", 3)
print(rex.describe())
rex.age = 4
print(rex.describe())

You should see: Rex is 3 years old then Rex is 4 years old. The object carries its data with it, and the method reads whatever is current at the moment it runs.

If not: if both lines say 3, you assigned to a copy rather than to rex — check you wrote rex.age = 4 and not age = 4. The second one just makes an unrelated variable and leaves the dog untouched.

🎉
Check yourself before moving on

Without scrolling up: you get TypeError: Dog.speak() takes 0 positional arguments but 1 was given. What is wrong, and where? Answer: speak was defined without self. Python always passes the object as the first argument, so every method must accept it.

Now do it without the page: write a BankAccount class holding an owner and a balance, with a deposit(amount) method that increases the balance and returns the new total. Same shape as step 4, plus one line that changes self.

Summary

  • Classes are blueprints; objects are instances created from classes
  • __init__ initializes new objects; self refers to the current instance
  • Instance attributes belong to each object; class attributes are shared
  • Dunder methods (__str__, __eq__, etc.) customize built-in behavior
  • Use underscore prefix (_name) to signal private attributes
  • Classes bundle related data and behavior together, making code organized and reusable
🎉
OOP fundamentals unlocked!

You can now create your own classes and objects. Next up: inheritance and polymorphism — building class hierarchies and sharing code between related classes.