Skip to content

How to Understand Python Classes and Objects Logically

The Problem

I saw a Reddit post where someone said they couldn’t understand classes and objects “clearly and logically.” They knew functions, loops, and conditionals, but classes felt weird with all that syntax.

I think this is a common problem. You learn procedural programming first:

procedural.py
# What you already understand
name = "Buddy"
age = 3
def bark(name):
print(f"{name} says woof!")
bark(name) # "Buddy says woof!"

Then you see this:

confusing_class.py
class Dog:
def __init__(self, name):
self.name = name
def bark(self):
print(f"{self.name} says woof!")

It feels weird because:

  • You don’t know when to use classes vs functions
  • The self parameter looks redundant
  • You can’t see what problem classes solve that functions can’t

I think the issue is that tutorials teach class syntax before the logical reasoning for WHY to use them. Let me show you how I think about classes.

What Classes Actually Do

I think of a class as a “super-function” that carries its own data.

Here’s the procedural way you already know:

procedural_dog.py
# Separate data and functions
dog1_name = "Buddy"
dog1_age = 3
dog2_name = "Max"
dog2_age = 5
def bark(name, age):
print(f"{name} says woof! Age: {age}")
def sleep(name, age):
print(f"{name} is sleeping. Age: {age}")
def eat(name, age, food):
print(f"{name} eats {food}. Age: {age}")
# Pass data to every function
bark(dog1_name, dog1_age)
sleep(dog2_name, dog2_age)
eat(dog1_name, dog1_age, "bone")

The problem is that every function needs name and age. When you add more functions, you pass the same variables everywhere.

Here’s the class approach:

class_dog.py
class Dog:
def __init__(self, name, age):
self.name = name # Store data ONCE
self.age = age
def bark(self):
print(f"{self.name} says woof! Age: {self.age}")
def sleep(self):
print(f"{self.name} is sleeping. Age: {self.age}")
def eat(self, food):
print(f"{self.name} eats {food}. Age: {self.age}")
# Create instances
dog1 = Dog("Buddy", 3)
dog2 = Dog("Max", 5)
# Data is automatically included
dog1.bark()
dog2.sleep()
dog1.eat("bone")

Now I can explain what’s happening:

  • A class bundles data (variables) + functions that use that data
  • Objects let you create multiple independent versions with their own data
  • self just means “this object’s data” (like saying “my name” instead of “the name”)

When I call dog1.bark(), Python automatically passes dog1 as the first parameter. So self becomes dog1, and self.name is dog1.name.

When to Use Classes vs Functions

I use functions when:

  • I have a simple task (calculate, transform, check)
  • Input → Process → Output
  • No need to remember state between calls
function_example.py
# Simple calculation - NO class needed
def calculate_discount(price, discount_percent):
return price * (1 - discount_percent / 100)
final_price = calculate_discount(100, 20) # $80

I don’t need a class because:

  • Single task
  • No state to track
  • Input → output and done

I use classes when:

  • I have MULTIPLE functions that share the SAME data
  • I need to track state (remember things between function calls)
  • I want to create multiple independent “things”
bank_account.py
class BankAccount:
def __init__(self, owner, initial_balance=0):
self.owner = owner
self.balance = initial_balance # Shared state
def deposit(self, amount):
self.balance += amount
print(f"Deposited ${amount}. New balance: ${self.balance}")
def withdraw(self, amount):
if amount <= self.balance:
self.balance -= amount
print(f"Withdrew ${amount}. New balance: ${self.balance}")
else:
print("Insufficient funds!")
def get_balance(self):
return self.balance
# Create multiple independent accounts
account1 = BankAccount("Alice", 1000)
account2 = BankAccount("Bob", 500)
account1.deposit(200) # Alice: $1200
account2.withdraw(100) # Bob: $400
account1.withdraw(50) # Alice: $1150

I use a class here because:

  • balance is shared across deposit, withdraw, get_balance
  • Multiple accounts track independent balances
  • State persists between function calls

A Real Example: Game Characters

Let me show you a before/after comparison that made classes click for me.

Procedural approach (gets messy):

procedural_game.py
# Game character without classes
player1_name = "Hero"
player1_health = 100
player1_x = 0
player1_y = 0
player2_name = "Villain"
player2_health = 80
player2_x = 10
player2_y = 10
def move(name, health, x, y, dx, dy):
x += dx
y += dy
print(f"{name} moved to ({x}, {y})")
return name, health, x, y # Pass all data around
def take_damage(name, health, x, y, damage):
health -= damage
print(f"{name} took {damage} damage. Health: {health}")
return name, health, x, y
# Every function needs ALL parameters
player1_name, player1_health, player1_x, player1_y = move(
player1_name, player1_health, player1_x, player1_y, 1, 1
)
player1_name, player1_health, player1_x, player1_y = take_damage(
player1_name, player1_health, player1_x, player1_y, 20
)

This is hard to maintain. Every function needs all the parameters, and I have to pass them back and forth.

OOP approach (clean and logical):

oop_game.py
class GameCharacter:
def __init__(self, name, health, x, y):
self.name = name
self.health = health
self.x = x
self.y = y
def move(self, dx, dy):
self.x += dx
self.y += dy
print(f"{self.name} moved to ({self.x}, {self.y})")
def take_damage(self, damage):
self.health -= damage
print(f"{self.name} took {damage} damage. Health: {self.health}")
player1 = GameCharacter("Hero", 100, 0, 0)
player2 = GameCharacter("Villain", 80, 10, 10)
player1.move(1, 1) # Data is self-contained!
player2.take_damage(20)
player1.take_damage(10)

Now the code is cleaner. Each player object carries its own data, and I don’t have to pass parameters around.

Common Mistakes I See

Mistake 1: Overusing Classes

I’ve seen people create a class for everything:

wrong_calculator.py
# WRONG: Class for simple calculation
class Calculator:
def add(self, a, b):
return a + b
calc = Calculator()
result = calc.add(5, 3) # Unnecessary!

This should just be a function:

right_calculator.py
# RIGHT: Simple function
def add(a, b):
return a + b
result = add(5, 3) # Simple and clear

Mistake 2: Misunderstanding self

I used to think self was special syntax. It’s not. It’s just the first parameter.

When I write:

dog1 = Dog("Buddy", 3)
dog1.bark()

Python actually does this:

Dog.bark(dog1) # Passes dog1 as the first argument

The method definition becomes:

def bark(self): # self = dog1
print(f"{self.name} says woof!") # dog1.name

I could name it anything (I’ve seen this, obj, s), but self is the convention.

Mistake 3: Learning Syntax Before Logic

I think the mistake is learning HOW to write classes before understanding WHAT problem they solve.

The logical problem is: When multiple functions need to share the same data, classes organize that code better than passing variables around.

The syntax (__init__, self) comes second.

How I Think About It Now

Here’s my mental model:

  • Functions = actions (verbs) - calculate, transform, check
  • Classes = things (nouns) that can perform actions - User, GameCharacter, BankAccount
  • Objects = specific instances of those things - my user account, this player, Alice’s bank account

When I find myself passing the same variables (name, age, balance, health) to many different functions, that’s when a class becomes logically useful.

Let me show you one more example:

shopping_cart.py
class ShoppingCart:
def __init__(self):
self.items = [] # Shared state
self.total = 0
def add_item(self, name, price):
self.items.append({"name": name, "price": price})
self.total += price
print(f"Added {name} (${price}). Total: ${self.total}")
def remove_item(self, name, price):
if {"name": name, "price": price} in self.items:
self.items.remove({"name": name, "price": price})
self.total -= price
print(f"Removed {name} (${price}). Total: ${self.total}")
def checkout(self):
print(f"Checking out {len(self.items)} items. Total: ${self.total}")
self.items = []
self.total = 0
cart1 = ShoppingCart()
cart2 = ShoppingCart()
cart1.add_item("Book", 20)
cart2.add_item("Laptop", 1000)
cart1.add_item("Pen", 2)
cart1.checkout()
cart2.checkout()

This makes sense because:

  • The cart needs to remember items between function calls
  • Multiple carts can exist independently
  • items and total are shared across add_item, remove_item, checkout

Summary

In this post, I showed how to understand Python classes and objects by connecting them to functions you already know. The key point is that classes organize code by grouping related data and functions together.

Use classes when multiple functions need to share the same data. Otherwise, stick with simple functions.

The logical bridge is:

  • Functions = actions (verbs)
  • Classes = things (nouns) that can perform actions
  • Objects = specific instances of those things

If you find yourself passing the same variables to many different functions, that’s when a class becomes logically useful.

Final Words + More Resources

My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me

Here are also the most important links from this article along with some further resources that will help you in this scope:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments