Object-Oriented Programming Basics

Learn the fundamentals of object-oriented programming: classes, objects, attributes, and methods.

~19 min Beginner

Thinking in objects

So far you have been writing code in a sequence, instruction by instruction. Object-oriented programming (OOP) is a different way to organize your code. Instead of thinking about steps, you think about objects.

An object is something that has properties, called attributes, and can do things, called methods. A dog has a name, a breed and an age. A dog can bark, eat and sit. In OOP, you create a blueprint called a class, and then you create individual objects from that blueprint.

  • Class: the blueprint or recipe, such as "Dog"
  • Object (instance): one real thing made from the blueprint, such as Rex
  • Attribute: data that belongs to the object, such as its name or age
  • Method: a function that belongs to the object, such as bark

An easy way to think about it: a cookie cutter is the class, and each cookie it makes is an object. They all share the same shape, but each cookie can have different decoration. Why bother? When a program grows, grouping related data and actions together keeps things organized. Python itself is built this way. Strings, lists and dictionaries are all objects with methods, and that is why you can write "hi".upper().

Creating a class

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

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

    def describe(self):
        print(f"{self.name} is a {self.breed} and is {self.age} years old.")

Break it down:

  • class Dog: starts the blueprint. Class names use CapitalizedWords by habit.
  • __init__ is the constructor. Python calls it automatically when you create a new object. The double underscores on both sides are part of the name.
  • self refers to the object itself. Every method gets it as the first parameter, and you do not pass it when calling.
  • self.name = name attaches the value to this specific object, so the object remembers it.

The class by itself does nothing yet. It is only the blueprint. You have to create an object from it, which is the next card.

Creating objects

dog1 = Dog("Rex", "German Shepherd", 3)
dog2 = Dog("Bella", "Labrador", 5)

dog1.bark()      # Rex says: Woof!
dog2.bark()      # Bella says: Woof!
dog1.describe()  # Rex is a German Shepherd and is 3 years old.

Writing Dog(...) with brackets creates a new object and runs __init__. The three values you pass go to name, breed and age (the self is filled in for you). Calling dog1.bark() runs the method with self set to dog1, which is why it says Rex.

Each object has its own separate copy of the attributes. Changing dog1.name does not affect dog2.name. You can make as many objects from one class as you like, and each one lives on its own.

Reading and changing attributes

print(dog1.name)   # Rex
dog1.age = 4       # change the age
print(dog1.age)    # 4

Use a dot to reach an attribute, both for reading and for changing. Changing dog1.age leaves dog2.age at 5, because they are separate objects. You can even add a new attribute to a single object after creating it, though it is cleaner to set everything in __init__.

A method can also change the object. Here is a small example, a Pet class with a birthday method:

class Pet:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def have_birthday(self):
        self.age += 1

rex = Pet("Rex", 3)
rex.have_birthday()
print(rex.age)   # 4

A method can also give back a value with return, just like a normal function. And if you print an object directly, you get an unhelpful text like <__main__.Dog object at 0x...>. You can fix that by adding a __str__ method that returns a nice string.

A practical example: a bank account

Objects shine when data and the actions on that data belong together. A bank account has a balance, and actions like deposit and withdraw must follow rules:

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

    def deposit(self, amount):
        if amount > 0:
            self.balance += amount
            print(f"Deposited ${amount:.2f}. New balance: ${self.balance:.2f}")
        else:
            print("Deposit amount must be positive.")

    def withdraw(self, amount):
        if amount > self.balance:
            print("Insufficient funds.")
        elif amount <= 0:
            print("Withdrawal amount must be positive.")
        else:
            self.balance -= amount
            print(f"Withdrew ${amount:.2f}. New balance: ${self.balance:.2f}")

    def show_balance(self):
        print(f"Account owner: {self.owner}")
        print(f"Current balance: ${self.balance:.2f}")


account = BankAccount("Alice", 1000)
account.show_balance()
account.deposit(500)
account.withdraw(200)
account.withdraw(2000)
account.show_balance()
Account owner: Alice
Current balance: $1000.00
Deposited $500.00. New balance: $1500.00
Withdrew $200.00. New balance: $1300.00
Insufficient funds.
Account owner: Alice
Current balance: $1300.00

The rules live inside the class. Nobody has to remember to check for enough money. They just call withdraw, and the object protects itself. Also notice the default value balance=0, which works exactly like default parameters in functions.

Inheritance

A class can inherit from another class. The child class gets all the attributes and methods of the parent, and can add its own or override existing ones:

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

    def speak(self):
        print("...")

class Dog(Animal):
    def speak(self):
        print(f"{self.name} says Woof!")

class Cat(Animal):
    def speak(self):
        print(f"{self.name} says Meow!")

animals = [Dog("Rex"), Cat("Whiskers"), Dog("Bella")]
for animal in animals:
    animal.speak()
Rex says Woof!
Whiskers says Meow!
Bella says Woof!

Writing class Dog(Animal) means Dog is a kind of Animal. Dog has no __init__ of its own, so it uses the parent's one. Both children override speak, so each gives its own answer. The loop calls the same method name on different objects and each behaves in its own way. That idea is called polymorphism, which is a fancy word for "many forms".

Inheritance saves you from copying code. Put what is shared in the parent, and only the differences in the children.

Making objects print nicely

Python has special methods with double underscores that let your objects behave like built-in ones. The most useful for beginners is __str__, which controls what print() shows:

class Book:
    def __init__(self, title, pages):
        self.title = title
        self.pages = pages

    def __str__(self):
        return f"{self.title} ({self.pages} pages)"

book = Book("Python Basics", 250)
print(book)   # Python Basics (250 pages)

Without __str__, you would see something like <__main__.Book object at 0x7f...>, which tells you nothing. Another handy one is __len__, which lets len(obj) work on your own objects.

When a child class has its own __init__ and still needs the parent's setup, call super().__init__(...):

class Puppy(Dog):
    def __init__(self, name, breed):
        super().__init__(name, breed, 0)   # age starts at 0

When to use classes

You do not need a class for everything. Here is a simple guide:

  • Use a function for a single action that takes input and gives output.
  • Use a dictionary for a simple bag of related values.
  • Use a class when you have several objects of the same kind, each with its own data, and actions that work on that data.

Good first ideas to practice with:

  • A Car with make, model and speed, and methods accelerate and brake
  • A Student with a name and a list of grades, and a method that returns the average
  • A Todo item with a description and a completed flag, and a method complete
class Student:
    def __init__(self, name):
        self.name = name
        self.grades = []

    def add_grade(self, grade):
        self.grades.append(grade)

    def average(self):
        return sum(self.grades) / len(self.grades)

s = Student("Maya")
s.add_grade(90)
s.add_grade(80)
print(s.average())   # 85.0

A common mistake is forgetting self. If you write def bark(): inside a class, Python complains that it got an unexpected argument. Another is writing name = name instead of self.name = name, so the value is never saved on the object.

Key takeaways

  • A class is a blueprint. An object is one thing made from it
  • Attributes are the data, and methods are the actions
  • __init__ runs automatically when an object is created and sets up its attributes
  • self means this particular object. Use self.name to store and read its data
  • Each object keeps its own copy of the attributes
  • A child class class Dog(Animal) inherits everything and can override methods
  • Practice by modeling real things: a Car, a Student, a Todo item
Sign up to track your progress Mark lessons as read and pick up where you left off Sign up free to unlock this practice Your first practice lesson is free to try. Create an account to unlock the rest and save your progress