Python Classes and Objects: The Complete Beginner's Guide

In Day 10 you learned reusable functions. Now you will learn classes and objects: how data and behavior are bundled into coherent units with \`__init__\`, \`self\`, methods, attributes, properties, and core OOP design principles.

Chapter 11 of 20 · Intermediate · 45 min · Python Programming Course

In Day 10 you learned that functions bundle related logic into a reusable unit. Classes take that idea one step further - they bundle related data and logic together into a single unit that represents a real-world concept.

A bank account has a balance, an owner, and operations like deposit and withdraw. A student has a name, scores, and behaviour like calculating an average. A product has a price, stock count, and actions like applying a discount. In each case, the data and the operations on that data naturally belong together.

Before classes, you would represent a student as a loose collection of variables and functions. With classes, you represent a student as a single object - one thing that knows its own data and knows what it can do with that data.

This is Object-Oriented Programming - the most widely used programming paradigm in professional software development.

What You Will Learn in This Chapter

By the end of this tutorial you will be able to:

  • Define classes using class
  • Write __init__ to initialise object attributes
  • Understand self and why it exists
  • Define instance methods that operate on object data
  • Distinguish instance attributes from class attributes
  • Write __str__ and __repr__ for readable object output
  • Use properties to add validation to attribute access
  • Understand encapsulation and why it matters
  • Write multiple classes that work together
  • Avoid the most common OOP mistakes beginners make

Estimated time: 45 minutes reading + 25 minutes practice

Classes vs Functions - What Changes and Why

Before writing a single line of OOP code, understand what problem it solves.

# Without classes - data and logic are separate
def calculate_average(student):
    return sum(student["scores"]) / len(student["scores"])

def assign_grade(student):
    avg = calculate_average(student)
    if avg >= 90: return "A"
    if avg >= 80: return "B"
    if avg >= 70: return "C"
    return "F"

def describe_student(student):
    avg = calculate_average(student)
    grade = assign_grade(student)
    return f"{student['name']}: {avg:.1f} ({grade})"

student1 = {"name": "Alice", "scores": [88, 92, 79]}
student2 = {"name": "Bob",   "scores": [72, 65, 81]}

print(describe_student(student1))
print(describe_student(student2))
# With classes - data and logic are bundled together
class Student:
    def __init__(self, name, scores):
        self.name = name
        self.scores = scores

    def calculate_average(self):
        return sum(self.scores) / len(self.scores)

    def assign_grade(self):
        avg = self.calculate_average()
        if avg >= 90: return "A"
        if avg >= 80: return "B"
        if avg >= 70: return "C"
        return "F"

    def describe(self):
        avg = self.calculate_average()
        grade = self.assign_grade()
        return f"{self.name}: {avg:.1f} ({grade})"

Defining a Class

class BankAccount:
    pass
account = BankAccount()
print(type(account))

The __init__ Method - Initialising Objects

__init__ is called automatically when you create a new instance:

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

Understanding self

self refers to the specific instance on which a method is called:

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

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

Why explicit self? account.deposit(500) is equivalent to BankAccount.deposit(account, 500).

Instance Attributes

Instance attributes are set on self and belong to each object independently:

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

Avoid adding attributes outside __init__, which makes object shape inconsistent.

Instance Methods

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

    def deposit(self, amount):
        if amount <= 0:
            return "Deposit amount must be positive"
        self.balance += amount
        self.transactions.append(f"+{amount}")
        return f"Deposited {amount}. New balance: {self.balance}"

    def withdraw(self, amount):
        if amount <= 0:
            return "Withdrawal amount must be positive"
        if amount > self.balance:
            return f"Insufficient funds. Balance: {self.balance}"
        self.balance -= amount
        self.transactions.append(f"-{amount}")
        return f"Withdrew {amount}. New balance: {self.balance}"

Class Attributes

Class attributes are shared across all instances:

class Student:
    school_name = "Schoolabe"
    student_count = 0

    def __init__(self, name, scores):
        self.name = name
        self.scores = scores
        Student.student_count += 1

Class Methods and Static Methods

class Student:
    def __init__(self, name, scores):
        self.name = name
        self.scores = scores

    @classmethod
    def from_string(cls, data_string):
        parts = data_string.split(",")
        name = parts[0].strip()
        scores = [int(s.strip()) for s in parts[1:]]
        return cls(name, scores)
class MathHelper:
    @staticmethod
    def is_prime(n):
        if n < 2:
            return False
        for i in range(2, int(n ** 0.5) + 1):
            if n % i == 0:
                return False
        return True

__str__ and __repr__

Implement readable output for users and developers:

class Student:
    def __init__(self, name, scores):
        self.name = name
        self.scores = scores

    def __str__(self):
        avg = sum(self.scores) / len(self.scores)
        return f"Student({self.name}, avg={avg:.1f})"

    def __repr__(self):
        return f"Student(name={self.name!r}, scores={self.scores!r})"

Properties - Controlled Attribute Access

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

    @property
    def balance(self):
        return self._balance

    @balance.setter
    def balance(self, amount):
        if amount < 0:
            raise ValueError("Balance cannot be negative")
        self._balance = amount

Properties are ideal for validation, read-only access, and computed values.

Encapsulation

Encapsulation means bundling data with methods and controlling access:

class Employee:
    def __init__(self, name, salary):
        self.name = name
        self._department = "General"
        self.__salary = salary

_ is internal-use convention. __ triggers name mangling.

Special Methods

Dunder methods let objects behave like built-ins:

class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __add__(self, other):
        return Vector(self.x + other.x, self.y + other.y)

    def __eq__(self, other):
        return self.x == other.x and self.y == other.y

A Complete Working Program

Here is a library management system where two classes work together:

class Book:
    def __init__(self, title, author, isbn):
        self.title = title
        self.author = author
        self.isbn = isbn
        self._is_available = True
        self._borrower = None

    @property
    def is_available(self):
        return self._is_available

    def checkout(self, member_name):
        if not self._is_available:
            return f"'{self.title}' is already checked out by {self._borrower}"
        self._is_available = False
        self._borrower = member_name
        return f"'{self.title}' checked out to {member_name}"

    def return_book(self):
        if self._is_available:
            return f"'{self.title}' was not checked out"
        borrower = self._borrower
        self._is_available = True
        self._borrower = None
        return f"'{self.title}' returned by {borrower}"


class Library:
    def __init__(self, name):
        self.name = name
        self._books = []

    def add_book(self, book):
        self._books.append(book)
        return f"Added: {book.title}"

    def available_books(self):
        return [b for b in self._books if b.is_available]

5 OOP Mistakes Every Beginner Makes

  1. Forgetting self in method definitions
  2. Using class attributes for mutable instance data
  3. Modifying state inside __str__
  4. Returning self from mutator methods without deliberate chaining design
  5. Making every attribute private unnecessarily

Practice: Classes and Objects Exercises

  • Exercise 1: Rectangle class with area(), perimeter(), is_square()
  • Exercise 2: Temperature class with validation property and computed units
  • Exercise 3: User class with class-level counter
  • Exercise 4: Product with __str__ and __repr__
  • Exercise 5: Author + Book classes working together

-> See all Python OOP exercises with solutions

What Comes Next - Day 12: Inheritance and Polymorphism

Day 12 covers:

  • Single and multiple inheritance
  • The super() function
  • Method overriding
  • Polymorphism and interfaces
  • Abstract classes
  • Inheritance vs composition

-> Continue to Day 12: Inheritance and Polymorphism

Frequently Asked Questions

What is a class in Python?

A class is a blueprint that defines object data (attributes) and behavior (methods).

What is the difference between a class and an object in Python?

A class is the definition; an object is an instance created from that definition.

What is __init__ in Python?

__init__ initializes an object immediately after creation.

Why does every method need self as the first parameter?

self gives the method access to the specific object instance it is operating on.

What is the difference between instance attributes and class attributes?

Instance attributes belong to each object; class attributes are shared across all objects.

What are __str__ and __repr__ in Python?

__str__ is user-facing display text; __repr__ is developer-facing representation.

What is encapsulation in Python?

Encapsulation bundles data with methods and controls external access via conventions and properties.

What is a property in Python?

A property adds controlled getter/setter behavior while preserving attribute-style access.

Chapter navigation

Frequently asked questions: Classes and objects

What is a class in Python?

A class is a blueprint that defines the structure and behavior of objects. It defines attributes (data) and methods (actions) that instances can use.

What is the difference between a class and an object in Python?

A class is the definition. An object (instance) is a concrete realization of that definition with actual data values.

What is __init__ in Python?

__init__ is a special method that runs automatically when an instance is created. It initializes instance attributes.

Why does every method need self as the first parameter?

self refers to the current instance. Python passes it automatically when you call an instance method, enabling access to that object’s attributes.

What is the difference between instance attributes and class attributes?

Instance attributes belong to each object independently. Class attributes are shared across all instances of the class.

What are __str__ and __repr__ in Python?

__str__ provides a human-readable string (used by print). __repr__ provides a developer-oriented representation (used by repr and the REPL).

What is encapsulation in Python?

Encapsulation bundles data with methods and controls access through conventions (_internal, __mangled) and managed interfaces such as properties.

What is a property in Python?

A property uses @property and optional setters/getters to validate or compute attribute values while preserving normal attribute access syntax.