DEV Community

Cover image for Python Classes Finally Made Sense: The Secret Behind Objects, Attributes, and Methods
Okall Omondi
Okall Omondi

Posted on

Python Classes Finally Made Sense: The Secret Behind Objects, Attributes, and Methods

If you're learning Python and you've reached classes, you may have had the same reaction I did:

“Okay... but why do I need all this?”

You already have variables, lists, dictionaries, and functions. They seem to do the job just fine.

So why introduce class, self, __init__(), inheritance, and all these other concepts?

The answer became much clearer to me when I stopped thinking about classes as complicated Python syntax and started thinking about them as a way to organize related data and behavior.

Let's break it down.

What Is a Class?

A class is essentially a blueprint for creating objects.

Imagine you're building an employee management system. Every employee might have:

  • A name
  • A salary
  • A department
  • A way to calculate their bonus
  • A way to get promoted

Instead of keeping all this information separately, we can define an Employee class:

class Employee:
    pass
Enter fullscreen mode Exit fullscreen mode

We can then create objects from that class:

navas = Employee()
emmanuel = Employee()
Enter fullscreen mode Exit fullscreen mode

Here, Employee is the class, while navas and emmanuel are objects (instances) of that class.

Think of it like this:

Class → Blueprint

Object → Something created from the blueprint

Adding Data with __init__()

We don't want to manually add every attribute after creating an employee.

That's where __init__() comes in.

class Employee:
    def __init__(self, name, salary, department):
        self.name = name
        self.salary = salary
        self.department = department
Enter fullscreen mode Exit fullscreen mode

Now creating an employee is much cleaner:

navas = Employee("Navas", 60000, "Marketing")
emmanuel = Employee("Emmanuel", 80000, "Engineering")
Enter fullscreen mode Exit fullscreen mode

The __init__() method runs automatically when an object is created.

One thing that confused me at first was the difference between name and self.name.

self.name = name
Enter fullscreen mode Exit fullscreen mode

The name on the right is the value passed into the method.

self.name is the attribute stored on the object.

So... What Exactly Is self?

self refers to the current object.

For example:

class Employee:
    def __init__(self, name, salary):
        self.name = name
        self.salary = salary

    def calculate_bonus(self):
        return self.salary * 0.10
Enter fullscreen mode Exit fullscreen mode

When we do:

navas.calculate_bonus()
Enter fullscreen mode Exit fullscreen mode

Python knows that self refers to navas.

That's why the method can access:

self.salary
Enter fullscreen mode Exit fullscreen mode

and use Navas's salary.

This is one of the biggest mental shifts when learning classes:

Methods operate on the object's current state.

Classes Can Have Behavior

Classes aren't just containers for data.

They can also contain methods that define what an object can do.

class Employee:
    def __init__(self, name, salary):
        self.name = name
        self.salary = salary

    def calculate_bonus(self):
        return self.salary * 0.10

    def give_raise(self, amount):
        self.salary += amount
Enter fullscreen mode Exit fullscreen mode

Now:

navas = Employee("Navas", 60000)

navas.give_raise(5000)

print(navas.salary)
print(navas.calculate_bonus())
Enter fullscreen mode Exit fullscreen mode

Navas's salary changes to 65000, and his bonus is calculated using his new salary.

This is where classes become really useful. Data and the operations that work on that data can live together.

Class Attributes vs Instance Attributes

There are two important types of attributes you'll encounter.

Instance attributes

These belong to a specific object:

self.name = name
self.salary = salary
Enter fullscreen mode Exit fullscreen mode

Navas and Emannuel can have different values.

Class attributes

These belong to the class itself:

class Employee:
    company = "LuxDev"
Enter fullscreen mode Exit fullscreen mode

Every employee can access it:

navas.company
emmanuel.company
Enter fullscreen mode Exit fullscreen mode

If you change the class attribute:

Employee.company = "Microsoft"
Enter fullscreen mode Exit fullscreen mode

objects that don't have their own company attribute will see the new value.

A simple rule:

Instance attributes describe the individual object. Class attributes describe something shared by the class.

Encapsulation: Controlling How Data Changes

Classes can also help us control how an object's data is modified.

For example, a bank account shouldn't allow a negative balance.

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

    def deposit(self, amount):
        if amount > 0:
            self._balance += amount

    def withdraw(self, amount):
        if 0 < amount <= self._balance:
            self._balance -= amount
Enter fullscreen mode Exit fullscreen mode

Instead of allowing anything to modify the balance directly, we provide methods that enforce rules.

Python also provides @property when you need more controlled access to attributes.

The bigger idea is:

Encapsulation means keeping an object's state consistent and controlling how it changes.

Inheritance: Reusing Existing Classes

What if we have different types of employees?

A manager is an employee, but a manager might have additional behavior.

class Manager(Employee):
    def manage_team(self):
        return f"{self.name} is managing the team."
Enter fullscreen mode Exit fullscreen mode

Manager inherits from Employee.

This means a manager can use the attributes and methods already defined in Employee.

We can also override behavior:

class Manager(Employee):
    def calculate_bonus(self):
        return self.salary * 0.20
Enter fullscreen mode Exit fullscreen mode

Now managers receive a different bonus calculation.

When we want to reuse the parent's implementation, we can use super():

class Manager(Employee):
    def __init__(self, name, salary, team_size):
        super().__init__(name, salary)
        self.team_size = team_size
Enter fullscreen mode Exit fullscreen mode

super().__init__() lets us reuse the parent's initialization logic instead of duplicating it.

Composition: Objects Can Contain Other Objects

Inheritance isn't the only way objects can relate to each other.

An employee can have an address.

class Address:
    def __init__(self, city):
        self.city = city


class Employee:
    def __init__(self, name, address):
        self.name = name
        self.address = address
Enter fullscreen mode Exit fullscreen mode

Now:

address = Address("Nairobi")
navas = Employee("Navas", address)

print(navas.address.city)
Enter fullscreen mode Exit fullscreen mode

This is called composition.

A useful way to remember the difference:

Inheritance: Manager is an Employee.

Composition: Employee has an Address.

A Few Python Class Features Worth Knowing

Python classes come with several useful features.

Class methods

Use @classmethod when the method needs to work with the class itself:

@classmethod
def change_company(cls, new_company):
    cls.company = new_company
Enter fullscreen mode Exit fullscreen mode

Here, cls refers to the class.

Static methods

Use @staticmethod when the method doesn't need either the object or class:

@staticmethod
def is_valid_salary(salary):
    return salary > 0
Enter fullscreen mode Exit fullscreen mode

Dunder methods

These are methods with double underscores, such as:

__init__()
__str__()
__repr__()
__eq__()
Enter fullscreen mode Exit fullscreen mode

They allow your objects to interact naturally with Python.

For example:

def __str__(self):
    return f"Employee: {self.name}"
Enter fullscreen mode Exit fullscreen mode

Now:

print(navas)
Enter fullscreen mode Exit fullscreen mode

can produce a useful description instead of Python's default object representation.

Don't Use Classes for Everything

This was another important lesson for me.

Classes aren't automatically better than functions.

If you're simply converting Celsius to Fahrenheit:

def celsius_to_fahrenheit(celsius):
    return (celsius * 9/5) + 32
Enter fullscreen mode Exit fullscreen mode

A function is perfectly fine.

Classes become useful when you have related data and behavior that belong together.

The Mental Model That Finally Clicked

After working through classes, this is the mental model I now use:

  • Class → blueprint
  • Object → instance created from the blueprint
  • Attributes → object's data/state
  • Methods → object's behavior
  • self → the current object
  • __init__() → initializes the object
  • Inheritance → "is-a" relationship
  • Composition → "has-a" relationship
  • Encapsulation → controlling how state changes
  • Polymorphism → different objects responding to the same method differently

And the most important idea:

A class brings related data and behavior together into an object that represents something meaningful.

For me, that made Python classes much less intimidating.

The goal isn't to memorize every class feature. It's to recognize when a problem naturally calls for an object and then use classes to model it.

Once that starts clicking, Object-Oriented Programming becomes much easier to reason about.

Top comments (0)