Python Series track

Week 07 · Intermediate Track

Week 7 — OOP Part 1: Classes & Instances

Model real-world entities with classes. Learn to define attributes in __init__, write instance methods, use properties for validation, and simplify data classes with @dataclass.

Intermediate Track2 min readFree lesson

Level: Intermediate | Prerequisite: Week 6

Learning Goals

  • Define classes and create instances
  • Understand the meaning of constructors and destructors
  • Work with instance methods and attributes
  • Encapsulation conventions (private, protected)

1. What is a class

A blueprint that combines data (attributes) and behaviors (methods). An instance (object) is created based on this.

class Account:
    def __init__(self, owner, balance=0):
        self.owner = owner       #Instance Properties
        self.balance = balance

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

    def withdraw(self, amount):
        if amount > self.balance:
            raise ValueError("Insufficient balance")
        self.balance -= amount

a = Account("Hong Gil-dong", 1000)    #Create Instance
a.deposit(500)
print(a.balance)   # 1500

2. Meaning of self

The first argument of a method is always self — the instance itself that was called.

a = Account("Hong Gil-dong")
a.deposit(100)        #In reality, Account.deposit(a, 100)

Through self, access instance attributes.

3. Instance variables vs Class variables

class Counter:
    total = 0   #Class variable — shared by all instances

    def __init__(self):
        Counter.total += 1
        self.id = Counter.total   #Instance variable — each is different

c1 = Counter()
c2 = Counter()
print(c1.id, c2.id, Counter.total)   # 1 2 2

4. Encapsulation conventions

Python has no true private. Represented by convention:

  • _single_underscore — "internal use" (signal not to use externally)
  • __double_underscore — name mangling (converted to _ClassName__attribute)
class Account:
    def __init__(self, owner, balance):
        self._owner = owner       #For internal use
        self.__balance = balance  # name mangled

    def get_balance(self):
        return self.__balance

a = Account("Hong Gil-dong", 1000)
print(a._owner)              #Possible (refrain as a convention)
# print(a.__balance)         # AttributeError
print(a._Account__balance)   #Operation is possible (not recommended)

5. Object comparison

Default equality compares whether objects are the same (using id). For semantic comparison, override eq (next week).

Key Examples (examples/)

File Content
Class definition & instance
Behavior of self
Class variables vs Instance variables
Encapsulation conventions

⚠️ Common Mistakes

  1. Omitting self — def deposit(amount): → An instance is automatically created, but if there are no parameters, TypeError occurs.
  2. Mutable object as a class variable — Shared by all instances, which can unintentionally change together.
    class Bag:
        items = []   #Danger!
    

→ Inside __init__, self.items = []. 3. Using an attribute with a different name in __init__ — Attribute missing due to a typo.

❓ FAQ

Q1. Do all codes have to be written as classes? A. No. Only when state + behavior should be bound together to make sense. Use simple functions if they are clearer.

Q2. Other magic methods besides __init__? A. In the next week, we will study __str__, __eq__, __len__, etc.

Q3. Naming convention for class names? A. PascalCase (BankAccount, UserProfile). Functions and variables use snake_case.

📝 Exercises (exercises/)

  • exercise1.md — BankAccount class
  • exercise2.md — Stack class
  • exercise3.md — Student class (Score Average/Highest)

Next Week

Week 8. Object-Oriented (2) — Inheritance · Polymorphism

Example files

The runnable files used in this lesson. Copy them into your own project and experiment.

examples/01_class_basic.pyPython
class Account:
    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:
            raise ValueError("Insufficient balance")
        self.balance -= amount

a = Account("Hong Gil-dong", 1000)
a.deposit(500)
print(a.balance)
a.withdraw(300)
print(a.balance)

b = Account("Kim Young-hee")  #balance default value 0
b.deposit(100)
print(b.balance)
examples/02_self.pyPython
class Greeter:
    def __init__(self, name):
        self.name = name

    def hello(self):
        return f"Hi, I'm {self.name}"

g = Greeter("Alice")
print(g.hello())            # Hi, I'm Alice

#The above call is actually the same as below
print(Greeter.hello(g))     # Hi, I'm Alice
examples/03_class_var.pyPython
class Counter:
    total = 0   #Class variable

    def __init__(self):
        Counter.total += 1
        self.id = Counter.total   #Instance variable

c1 = Counter()
c2 = Counter()
c3 = Counter()
print(c1.id, c2.id, c3.id)   # 1 2 3
print(Counter.total)         # 3

#Dangerous pattern — list in class variable
class Bag:
    items = []

b1 = Bag()
b2 = Bag()
b1.items.append("apple")
print(b2.items)   #['apple'] — also visible in b2!

#Correct pattern
class Bag2:
    def __init__(self):
        self.items = []

b3 = Bag2()
b4 = Bag2()
b3.items.append("apple")
print(b4.items)   # []
examples/04_encapsulation.pyPython
class Account:
    def __init__(self, owner, balance):
        self._owner = owner       #For internal use (conventional)
        self.__balance = balance  # name mangled

    def get_balance(self):
        return self.__balance

    def deposit(self, amount):
        if amount <= 0:
            raise ValueError("Amount is positive")
        self.__balance += amount

a = Account("Hong Gil-dong", 1000)
print(a.get_balance())
print(a._owner)        #Possible (refrain as a convention)

try:
    print(a.__balance)
except AttributeError as e:
    print("Blocked:", e)

#Bypass possible (not recommended)
print(a._Account__balance)

Assignment

Requirements

  • Attributes: owner, balance
  • Methods:
    • __init__(owner, balance=0)
  • deposit(amount) — Only allows positive numbers
  • withdraw(amount) — Exception if insufficient balance
  • transfer(other_account, amount) — Transfer to another account
  • All validation failures result in ValueError

Example of use

a = BankAccount("Alice", 1000)
b = BankAccount("Bob")
a.transfer(b, 300)
print(a.balance, b.balance)   # 700 300

Assignment 2. Stack Class

Requirements

  • LIFO (Last In, First Out)
  • Methods: push(x), pop(), peek(), is_empty(), __len__()
  • Exception when popping/peeking from an empty stack

Example of use

s = Stack()
s.push(1); s.push(2); s.push(3)
print(s.peek())   # 3
print(len(s))     # 3
print(s.pop())    # 3

Assignment 3. Student Class

Requirements

  • Attributes: name, scores: list[int]
  • Methods:
    • add_score(s)
  • average() — Average
  • highest() — Highest score
  • passed(threshold=60) — True if average is above threshold

Example of use

s = Student("Hong Gil-dong")
s.add_score(85); s.add_score(92); s.add_score(78)
print(s.average())   # 85.0
print(s.passed())    # True
Show the reference solution

Try the assignment yourself first — comparing your attempt with a working solution is where most of the learning happens.

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

    def deposit(self, amount):
        if amount <= 0:
            raise ValueError("Deposit is positive")
        self.balance += amount

    def withdraw(self, amount):
        if amount <= 0:
            raise ValueError("Withdrawal is positive")
        if amount > self.balance:
            raise ValueError(f"Insufficient balance: {self.balance} < {amount}")
        self.balance -= amount

    def transfer(self, other, amount):
        self.withdraw(amount)
        other.deposit(amount)

a = BankAccount("Alice", 1000)
b = BankAccount("Bob")
a.transfer(b, 300)
print(a.balance, b.balance)
solution2.pyPython
class Stack:
    def __init__(self):
        self._items = []

    def push(self, x):
        self._items.append(x)

    def pop(self):
        if not self._items:
            raise IndexError("pop from empty stack")
        return self._items.pop()

    def peek(self):
        if not self._items:
            raise IndexError("peek from empty stack")
        return self._items[-1]

    def is_empty(self):
        return not self._items

    def __len__(self):
        return len(self._items)

s = Stack()
s.push(1); s.push(2); s.push(3)
print(s.peek())
print(len(s))
print(s.pop())
print(s.pop())
print(s.is_empty())
print(s.pop())
print(s.is_empty())
solution3.pyPython
class Student:
    def __init__(self, name):
        self.name = name
        self.scores = []

    def add_score(self, s):
        self.scores.append(s)

    def average(self):
        return sum(self.scores) / len(self.scores) if self.scores else 0

    def highest(self):
        return max(self.scores) if self.scores else 0

    def passed(self, threshold=60):
        return self.average() >= threshold

s = Student("Hong Gil-dong")
for x in (85, 92, 78):
    s.add_score(x)
print(s.average())
print(s.highest())
print(s.passed())

Learn this lesson with a mentor

Work through the Python Series track on your own, or join a guided cohort at our Pitampura campus and online.

Call NowEnroll Now