OOP in Python: Classes, Composition, and Clean Service Design

#Clean Code #FastAPI #OOP #Python #Software Architecture

Object-Oriented Programming (OOP) in Python is often misunderstood by developers transitioning from strictly object-oriented languages like Java, C#, or PHP. Because Python supports multiple paradigms โ€” imperative, functional, and object-oriented โ€” it gives developers freedom in how they structure code. However, building maintainable backend applications requires mastering Python's class model, magic methods, dataclasses, and composition principles.

1. Python Classes vs Java/PHP Classes

In Python, everything is an object, including classes, functions, and modules. Unlike PHP or Java, Python does not have explicit private or protected keywords. Access control relies on naming conventions:

  • public_attribute: Publicly accessible.
  • _protected_attribute: Internal convention (indicates "do not touch outside this module").
  • __private_attribute: Triggers name mangling (prevents accidental overrides in subclasses).

2. Modern Python Data Containers: dataclasses & Pydantic

Creating standard boilerplate classes for holding state can be verbose in classic OOP. Python 3.7 introduced @dataclass to eliminate boilerplate __init__ and __repr__ methods.

from dataclasses import dataclass, field
from datetime import datetime

@dataclass
class CustomerOrder:
    id: str
    customer_email: str
    total_amount: float
    created_at: datetime = field(default_factory=datetime.utcnow)

    def calculate_tax(self, rate: float = 0.18) -> float:
        return round(self.total_amount * rate, 2)

For API request payloads and external data parsing, use Pydantic models; for internal domain objects and value objects, use standard Python @dataclass.

3. Composition Over Inheritance in Service Design

Inheritance creates tight coupling. In modern Python backend architecture, favor composition โ€” composing services by injecting smaller, focused objects via dependency injection.

Bad: Deep Inheritance Chain

class BaseService:
    def log(self, msg): print(msg)

class BaseUserService(BaseService):
    def get_db(self): return DB()

class UserService(BaseUserService):
    def create(self): ...

Good: Clean Composition with Protocols (Interfaces)

Python's typing.Protocol provides structural subtyping (duck typing with static type checks). It acts like an interface in PHP or TypeScript without requiring explicit inheritance.

from typing import Protocol

class NotificationSender(Protocol):
    async def send(self, recipient: str, message: str) -> bool:
        ...

class EmailNotificationSender:
    async def send(self, recipient: str, message: str) -> bool:
        # Implementation for email sending
        return True

class SmsNotificationSender:
    async def send(self, recipient: str, message: str) -> bool:
        # Implementation for SMS sending
        return True

class UserRegistrationService:
    def __init__(self, notifier: NotificationSender):
        self.notifier = notifier

    async def register_user(self, email: str):
        # Register user logic...
        await self.notifier.send(email, "Welcome to our platform!")

4. Designing a Clean Service Layer

To keep controller/route handlers thin, encapsulate business logic inside dedicated service classes. A well-designed Python service class should have:

  1. Explicit dependencies passed into __init__.
  2. Single-responsibility public methods.
  3. No direct coupling to web framework request/response objects (FastAPI, Flask, etc.).
class OrderProcessingService:
    def __init__(self, db_session, payment_gateway, mailer):
        self.db = db_session
        self.payment_gateway = payment_gateway
        self.mailer = mailer

    async def process_checkout(self, order_id: str, payment_token: str):
        order = await self.db.get_order(order_id)
        payment_result = await self.payment_gateway.charge(order.total, payment_token)

        if payment_result.success:
            order.mark_as_paid()
            await self.db.save(order)
            await self.mailer.send_receipt(order.customer_email, order.id)
            return order
        else:
            raise PaymentFailedException(payment_result.error_message)

Summary

Mastering OOP in Python means embracing Pythonic paradigms: using type hints, leveraging protocols over rigid inheritance, employing dataclasses for state, and building modular service classes via composition. This keeps your codebase testable, maintainable, and ready for production scale.

Looking to structure your Python services with clean architecture? Explore my AI integration development services or Hire Python Developer to discuss software design consultation.


Prakash Tank

Prakash Tank

Full-Stack Architect & Tech Enthusiast. Passionate about building scalable applications and sharing knowledge with the community.