DEV Community

Cover image for Build Your Own Types: Classes Explained Simply
Akhilesh
Akhilesh

Posted on

Build Your Own Types: Classes Explained Simply

You have been using types this whole time.

Strings. Integers. Lists. Dictionaries. Every one of those is a type. And they don't just hold data. They also come with built-in behaviors. Strings have .upper(). Lists have .append(). Dictionaries have .get().

Now here is the question. What if the type you need doesn't exist yet? What if you want a Student type that holds a name, age, and scores, and also knows how to calculate its own average?

That is exactly what classes let you build.


The Cookie Cutter Idea

A class is a blueprint. It describes what something looks like and what it can do. By itself it's just a description, it doesn't actually exist yet.

An object is what you get when you use the blueprint to create something real. You can create as many objects from one class as you want. Same structure, different data.

Cookie cutter is the class. The cookies are the objects.


Your First Class

class Student:
    def __init__(self, name, age):
        self.name = name
        self.age = age
Enter fullscreen mode Exit fullscreen mode

class Student: defines a new type called Student.

def __init__ is a special method that runs automatically when you create a new Student. Think of it as the setup instructions.

self refers to the specific object being created. When you make a Student named Alex, self is that Alex object. When you make a Student named Priya, self is that Priya object. Same class, different self.

self.name = name stores the name on this specific object. Without self., the value disappears when __init__ finishes.

Now create some students.

student1 = Student("Alex", 25)
student2 = Student("Priya", 22)

print(student1.name)
print(student2.name)
print(student1.age)
Enter fullscreen mode Exit fullscreen mode

Output:

Alex
Priya
25
Enter fullscreen mode Exit fullscreen mode

Two objects. Same blueprint. Different data.


Adding Methods

Methods are functions that belong to a class. They define what the object can do.

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

    def greet(self):
        print(f"Hi, I'm {self.name} and I'm {self.age} years old.")

    def birthday(self):
        self.age = self.age + 1
        print(f"Happy birthday {self.name}! You are now {self.age}.")
Enter fullscreen mode Exit fullscreen mode
student1 = Student("Alex", 25)
student1.greet()
student1.birthday()
student1.greet()
Enter fullscreen mode Exit fullscreen mode

Output:

Hi, I'm Alex and I'm 25 years old.
Happy birthday Alex! You are now 26.
Hi, I'm Alex and I'm 26 years old.
Enter fullscreen mode Exit fullscreen mode

birthday() changed self.age on that specific object. student1 is now 26. Any other Student object you created is still whatever age they were. The change only affected student1.


A More Real Example

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

    def deposit(self, amount):
        self.balance = self.balance + amount
        print(f"Deposited {amount}. New balance: {self.balance}")

    def withdraw(self, amount):
        if amount > self.balance:
            print("Not enough money.")
        else:
            self.balance = self.balance - amount
            print(f"Withdrew {amount}. New balance: {self.balance}")

    def show_balance(self):
        print(f"{self.owner}'s balance: {self.balance}")
Enter fullscreen mode Exit fullscreen mode
account = BankAccount("Alex", 1000)
account.show_balance()
account.deposit(500)
account.withdraw(200)
account.withdraw(2000)
Enter fullscreen mode Exit fullscreen mode

Output:

Alex's balance: 1000
Deposited 500. New balance: 1500
Withdrew 200. New balance: 1300
Not enough money.
Enter fullscreen mode Exit fullscreen mode

Notice how the object remembers its state between method calls. self.balance keeps updating. Each call to deposit or withdraw works with the current balance, not some starting value. That persistent memory is what makes objects powerful.


Multiple Objects, Independent State

account1 = BankAccount("Alex", 1000)
account2 = BankAccount("Priya", 5000)

account1.deposit(200)
account2.withdraw(1000)

account1.show_balance()
account2.show_balance()
Enter fullscreen mode Exit fullscreen mode

Output:

Deposited 200. New balance: 1200
Withdrew 1000. New balance: 4000
Alex's balance: 1200
Priya's balance: 4000
Enter fullscreen mode Exit fullscreen mode

Two completely separate accounts. Operations on one never touch the other. That independence is the whole point of objects.


The str Method

When you print an object, Python normally shows something ugly like <__main__.Student object at 0x10b3f2a90>. You can fix that.

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

    def __str__(self):
        return f"Student: {self.name}, Age: {self.age}"

student1 = Student("Alex", 25)
print(student1)
Enter fullscreen mode Exit fullscreen mode

Output:

Student: Alex, Age: 25
Enter fullscreen mode Exit fullscreen mode

__str__ is another special method like __init__. Python calls it automatically whenever it needs to display your object as text. Return a string from it and that's what gets printed.


Three Things That Confuse Everyone

Forgetting self in method definitions.

class Student:
    def __init__(name, age):     # missing self
        self.name = name
Enter fullscreen mode Exit fullscreen mode
TypeError: Student.__init__() takes 2 positional arguments but 3 were given
Enter fullscreen mode Exit fullscreen mode

Every method in a class must have self as the first parameter. Every single one. Even if you don't use self inside the method. Python passes the object automatically as the first argument, so self has to be there to receive it.

Forgetting self when storing data.

def __init__(self, name):
    name = name       # wrong, this just creates a local variable
Enter fullscreen mode Exit fullscreen mode
def __init__(self, name):
    self.name = name  # correct, this stores it on the object
Enter fullscreen mode Exit fullscreen mode

Without self., the data exists only while __init__ is running and then vanishes. Always use self. to store anything you need to keep.

Using the class itself instead of creating an object.

Student.greet()      # wrong, Student is the blueprint
student1.greet()     # correct, student1 is the actual object
Enter fullscreen mode Exit fullscreen mode

Try This

Create classes_practice.py.

Build a class called Product for an online store.

It should store: name, price, and quantity in stock.

It should have these methods:

  • show_info() that prints all the product details neatly
  • sell(quantity) that reduces the stock by that amount, but prints "not enough stock" if the quantity requested is more than what's available
  • restock(quantity) that adds to the stock
  • apply_discount(percent) that reduces the price by that percentage and prints the new price

Create two different products. Call different methods on each. Make sure the state changes correctly and the two products stay independent.


What's Next

Objects are great for organizing data and behavior together. But Python also needs to work with data that lives in files, not just in memory. Next post is about reading and writing files, which is how your programs start talking to the real world.

Top comments (0)