← Back to list

Python Class Types -Explained Simply (Python Classes Internals — Post 1/7)

If you are new to Python (or not-so-new and still confused about classes), this post is for you. It explains six different kinds of classes…

Zeba Tasneem · 2026-03-07 09:19 · 0 claps · 7.1 min read
#python-class #typeclass
Open on Medium ↗

Python Class Types -Explained Simply (Python Classes Internals — Post 1/7)

If you are new to Python (or not-so-new and still confused about classes), this post is for you. It explains six different kinds of classes in Python, what problem each one solves, and when you should use which one.

What is a Class? (Quick Refresher)

A class is a blueprint for creating objects.

class Dog:
    def __init__(self, name):
        self.name = name

def bark(self):
        return "Woof!"
d = Dog("Buddy")
print(d.bark())   # Woof!

This is like defining a blueprint for a dog — every dog has a name and can bark(), and creating an object (like d) means you make a specific dog from that blueprint.

In Python, every class silently inherits from object:

class Dog:
    pass

#is the same as:

class Dog(object):
    pass

*class Dog: is the same as class Dog(object): — every class in Python inherits from object by default.*

Class Types: Six Types

There are six common patterns for classes in Python. You don’t need to memorize all of them, just know what they are and when each one is useful.

1. Regular Class

Use this when: You want to model any normal thing — a person, product, order, configuration, service, etc.

What it is: A plain class that does not try to do anything fancy. It stores data (__init__) and implements behavior (methods).

class BankAccount:
    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 funds")
        self.balance -= amount\

acc = BankAccount("Sherry", 1000)
acc.deposit(500)
acc.withdraw(200)
print(acc.balance)

This small BankAccount example shows how a class organizes data and behavior together in one place.

  • __init__ is the setup part — it runs when you create the account (acc = BankAccount("Sherry", 1000)).
  • deposit and withdraw are actions — they tell the account what you can do with the money.
  • The withdraw method also protects the account — it checks if you have enough money before allowing withdrawal. If not, it shows an error.

In simple terms: A class is like a mini‑program inside a box — it holds data (owner, balance) and safe rules /method(deposit, withdraw) that work together.

2. Child Class (Inheritance)

Use this when: One thing is a more specific version of another, for example, Car is a kind of Vehicle.

What it is: A class that inherits from another class, so it reuses and extends behavior.

class Vehicle:
    def __init__(self, brand):
        self.brand = brand
    def start(self):
        return "Vehicle started"

class Car(Vehicle):              # Car extends Vehicle
    def honk(self):
        return "Beep beep!"
my_car = Car("Toyota")
print(my_car.start())   # Vehicle started
print(my_car.honk())
  • Vehicle is a base class with common behavior (start).
  • Car(Vehicle) means Car inherits from Vehicle, so it automatically gets brand and start().
  • Car adds its own method honk(), showing reuse + extension — the classic idea of inheritance.

In simple terms: Car is a special kind of Vehicle that can start (like any vehicle) and also honk (like a car

Important : Always call super().__init__() in the child class:

class ElectricCar(Car):
    def __init__(self, brand, battery):
        super().__init__(brand)        # ✅ initialize parent first
        self.battery = battery
  • Always call super().__init__() in the child class so that the parent class is properly set up (like storing brand) before adding child‑specific data (like battery).

In simple terms: super().__init__(brand) means: First do what the parent (Car / Vehicle) does when creating an object, then add the extra details for ElectricCar.

3. Abstract Class

Use this when: You want to force other classes to implement certain methods, for example, payment gateways that must all provide pay() and refund().

What it is: A class that cannot be instantiated directly — it only exists as a template for others.

from abc import ABC, abstractmethod

class PaymentGateway(ABC):
    @abstractmethod
    def pay(self, amount):
        pass
    @abstractmethod
    def refund(self, amount):
        pass

class Razorpay(PaymentGateway):
    def pay(self, amount):
        return f"Paid ₹{amount} via Razorpay"
    def refund(self, amount):
        return f"Refunded ₹{amount} via Razorpay"

# PaymentGateway()  ❌ TypeError - abstract class cannot be instantiated

This example shows how to enforce a contract in Python:

  • from abc import ABC, abstractmethod gives you the tools to create abstract classes — special classes that cannot be created directly.
  • PaymentGateway is an abstract base class. It declares that every payment gateway must implement .pay() and .refund(), but it does not provide their implementation.
  • Razorpay inherits from PaymentGateway and must implement both methods — if it doesn’t, Python will raise TypeError when you try to instantiate it.
  • The last line (PaymentGateway() is commented with TypeError) tells you: you can’t create an object from PaymentGateway itself — you must use concrete child classes like Razorpay.

In simple terms: An abstract class is a ‘template’ with rules. You can’t use it directly, but you must follow its rules when you create real classes from it. Use an abstract class when you want to enforce a contract — “if you inherit from me, you must implement pay and refund.”

4. Data Class

Use this when: Your class is mostly about storing data — not complex behavior. You want to avoid writing __init__, __repr__, etc., manually.

What it is: A class that is automatically generated with __init__, __repr__, __eq__, and more.

from dataclasses import dataclass, field
@dataclass
class Employee:
    name: str
    department: str
    salary: float = 0.0
    skills: list = field(default_factory=list)
emp = Employee("Sherry", "Engineering", 80000.0)
print(emp)
# Employee(name='Sherry', department='Engineering', salary=80000.0, skills=[])

@dataclass automatically:

  • Creates classe __init__ so you can write Employee("Sherry", "Engineering", 80000.0)
  • Creates __repr__ so print(emp) shows a nice, readable line like Employee(name='Sherry', department='Engineering', salary=80000.0, skills=[])
  • Handles defaults and mutable‑default‑safe fields (via field(default_factory=list)).
# Without dataclass (same behavior, more code)
from typing import List

class Employee:
    def __init__(self, name: str, department: str, salary: float = 0.0, skills: List[str] = None):
        self.name = name
        self.department = department
        self.salary = salary
        self.skills = skills if skills is not None else []

    def __repr__(self):
        return (f"Employee(name='{self.name}', department='{self.department}', "
                f"salary={self.salary}, skills={self.skills})")

emp = Employee("Sherry", "Engineering", 80000.0)
print(emp)
# Employee(name='Sherry', department='Engineering', salary=80000.0, skills=[])

Edge cases you can control:

# Make it immutable:
@dataclass(frozen=True)
class Config:
    host: str
    port: int
# config.host = "new"   ❌ FrozenInstanceError

This small example shows two important things data classes can do:

  • frozen=True makes the class immutable — once you create a Config object, you cannot change any of its fields. If you try, Python will raise an error, which is good if this is a configuration that should never be modified after setup.
# Validate data:
@dataclass
class Product:
    price: float
    def __post_init__(self):
        if self.price < 0:
            raise ValueError("Price cannot be negative")

# Create valid object
p = Product(10.5)
print(p)      # Product(price=10.5)

# This will raise ValueError
# p_invalid = Product(-5)   ❌ ValueError: Price cannot be negative
  • __post_init__ lets you validate the data after the object is created. For example, Product checks that price is not negative right after __init__ finishes. This is very useful when you want to catch invalid data early, instead of letting bugs propagate silently.

Use a data class when the class is data-firstEmployee, Student, Config, Point(x, y) — not a heavy behavior object.

5. Singleton Class

Use this when: You want only one instance to exist — for example, a database connection, a logger, or a global configuration.

What it is: A class that returns the same object every time it is created.

class DatabaseConnection:
    _instance = None

    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance
db1 = DatabaseConnection()
db2 = DatabaseConnection()
print(db1 is db2)   # True

This is the basic singleton pattern:

  • The class stores one shared instance in cls._instance.
  • Every time you call DatabaseConnection(), it first checks if an instance already exists.
  • If not, it creates one and saves it; otherwise it returns the existing one.
  • db1 is db2 prints True, which means they are the same object in memory, not two different ones.

In simple terms: This is like having one shared database connection that the whole program reuses instead of creating new ones every time.

Thread‑safe version:

import threading
class ThreadSafeSingleton:
    _instance = None
    _lock = threading.Lock()
    def __new__(cls):
        with cls._lock:
            if cls._instance is None:
                cls._instance = super().__new__(cls)
        return cls._instance

This is the thread‑safe singleton version:

  • It adds threading.Lock() so that only one thread at a time can enter the critical section.
  • The with cls._lock: block protects the check‑and‑create logic.
  • This prevents race conditions when two threads try to create the instance at the same time.

In simple terms: The basic singleton works fine in single‑threaded code, the thread‑safe version is what you use when your program runs in multi‑threaded environments (like web servers) and you still want only one shared instance.

6. Metaclass (Brief Introduction)

Use this when: You want to control how classes themselves are created (not object behavior). Used by Django ORM, SQLAlchemy, Enum

A metaclass is a class that creates classes

Simple hierarchy:

Class → creates objects
Metaclass → creates classes

Example:

class Dog:
    pass

d = Dog()

print(type(d))   # Dog
print(type(Dog)) # type

Here:

  • Dog is a class
  • d is an object
  • type is the metaclass that created the Dog class

When we write a class normally:

class Dog:
    sound = "Woof"

Python internally does something similar to this:

Dog = type("Dog", (), {"sound": "Woof"})

Here:

  • "Dog" → class name
  • () → parent classes
  • {} → attributes and methods of the class

So **type builds the class. In Python the default metaclass is `type`**.

Why Metaclasses Exist

Since type creates classes, we can customize class creation.

This means we can:

  • automatically add methods
  • enforce rules on classes
  • register classes automatically
  • modify class attributes

Frameworks often use this technique. Examples: Django ORM registers models automatically

Creating a Custom Metaclass

A custom metaclass inherits from type.

Usually we override **__new__**, which runs when the class is being created.

Example:

class AutoGreet(type):
    def __new__(mcs, name, bases, dct):
        dct["greet"] = lambda self: f"Hello from {name}!"
        return super().__new__(mcs, name, bases, dct)

# tell Python to use our metaclass AutoGreet
class Person(metaclass=AutoGreet):
    pass

#Create an object:
p = Person()
print(p.greet())

# Output: Hello from Person!

Parameters:

  • mcs → the metaclass itself
  • name → class name
  • bases → parent classes
  • dct → dictionary containing class attributes and methods

Here we modify the dictionary before the class is created.

Even though the Person class never defined greet(), the metaclass automatically added the method when the class was created.

What Happens Internally

When Python sees this:

class Person(metaclass=AutoGreet):
    pass

It roughly does something like:

Person = AutoGreet("Person", (), {})
AutoGreet.__new__(AutoGreet, "Person", (), {}

In Simple Terms

Think of it like a factory system:

AutoGreet (Metaclass)
        ↓
creates Person (Class)
        ↓
creates p (Object)

A Quick Reference Guide

Series Roadmap:

**Post 1/7** — "Python Classes → Internals" series

Next → **Post 2**: __new__ vs __init__ — how objects get built

Full roadmap:
Post 1: Types of Classes ✅
Post 2: __new__ vs __init__
Post 3: object - The Root of Every Class 
Post 4: type - class factory
Post 5: type vs object
Post 6: metaclasses  
Post 7: PyObject/PyTypeObject internals

Hope you'll find this journey helpful! 🐍

메타데이터
post_id
cfc9924ef44c
slug
python-class-types-explained-simply-cfc9924ef44c
url
https://medium.com/@Zeba_/python-class-types-explained-simply-cfc9924ef44c
canonical_url
https://medium.com/@Zeba_/python-class-types-explained-simply-cfc9924ef44c
author_url
https://medium.com/@Zeba_
status
ok
fetched_at
2026-06-12 18:14:10