Have you ever written dozens of scattered variables to manage products in an e-commerce system, ending up with code only you understand — and only if you read it before noon? We faced the same problem when we took over the ERP system of Hibrido Abbigliamento: dozens of price lists, sizes, seasons, all needing cross-referencing. Without a solid structure, every change becomes a nightmare. Object-oriented programming in Python is that organized toolbox that turns chaos into something maintainable, testable, and — most importantly — economically sensible.
We at Meteora Web work daily with Python to automate processes, build platforms, and integrate data. In this guide, you will find no textbook definitions: we start from messy code and clean it together, step by step.
Why Classes Save Revenue (and Sanity)
A program managing products, orders, or customers without classes looks like a warehouse where every box is thrown in the middle of the hallway. It works until you have three products. As you grow, you lose everything.
Classes allow you to encapsulate data and behavior in a single object. If a product has a name, price, quantity, and a function to calculate discounts, put them together. The code becomes self-documenting, and when something changes — like the VAT calculation — you modify one spot.
The simplest class that makes sense
Here is how we define a Product class for our e-commerce, with no frills:
class Product:
def __init__(self, name: str, price: float, qty: int) -> None:
self.name = name
self.price = price
self.qty = qty
def stock_value(self) -> float:
return self.price * self.qty
def apply_discount(self, percent: float) -> None:
self.price *= (1 - percent / 100)__init__ is the constructor: it runs when you create a new product. self is the reference to the current instance — each object has its own copies of data. Methods stock_value and apply_discount operate on that data.
Let’s test it:
tshirt = Product("White T-shirt", 29.90, 50)
print(tshirt.stock_value()) # 1495.0
tshirt.apply_discount(20)
print(tshirt.price) # 23.92A method that updates the price? Dangerous if not validated. In real life we add validation — but the concept is clear: data and operations are logically coupled.
Common mistakes to avoid immediately
- Do not use global variables for state — each instance should have its own attributes.
- Do not forget __init__ — if you omit it, you can still add attributes later, but chaos ensues.
- Do not mix business logic with IO — the class should return values, not print to screen.
What to do right now: Open an editor and create your first class with __init__ and at least one method that modifies internal state. Then instantiate two objects with different data and call the methods.
Inheritance: Don't Write the Same Logic Twice
In the real world you have products with different behaviors: a clothing item has size and color, a digital course has duration and number of lessons. Without inheritance, you duplicate code and multiply errors.
Inheritance lets you define a base class (e.g., Product) and then specialize. The child class inherits everything and can add or override methods.
Concrete example: clothing vs digital
Starting from our previous Product class, let’s create two child classes:
class ClothingProduct(Product):
def __init__(self, name: str, price: float, qty: int,
size: str, color: str) -> None:
super().__init__(name, price, qty)
self.size = size
self.color = color
def description(self) -> str:
return f"{self.name} - Size {self.size}, {self.color}"
class DigitalProduct(Product):
def __init__(self, name: str, price: float, duration_hours: float) -> None:
super().__init__(name, price, qty=99999) # virtual stock
self.duration_hours = duration_hours
def stock_value(self) -> float:
# For digital, stock is infinite; return price * 1
return self.priceNotice: super().__init__ calls the parent constructor. Methods stock_value are overridden to adapt the logic to the product type.
Inheritance in action:
shirt = ClothingProduct("Blue Shirt", 45.00, 20, "L", "blue")
print(shirt.stock_value()) # 900.0 (uses inherited method)
print(shirt.description()) # "Blue Shirt - Size L, blue"
course = DigitalProduct("Python Course", 199.00, 40.5)
print(course.stock_value()) # 199.0 (uses override)When inheritance becomes a problem
We have seen too many projects with five-level inheritance that nobody understood anymore. Rule of thumb: if a child class uses almost nothing from the parent, you might be forcing a wrong hierarchy. Prefer composition (an object containing another object) over excessive inheritance.
What to do right now: Start with a base class and create two derived classes with at least one overridden method. Verify polymorphism works: call the same method on objects of different types and get different results.
Dataclass: When Writing __init__ is Boring (and Risky)
Dataclasses were introduced in Python 3.7 to reduce boilerplate. When a class is only meant to hold data, with few methods, a dataclass saves you from manually writing __init__, __repr__, __eq__, and other special methods.
We often use them for DTOs (Data Transfer Objects) — for instance, data coming out of a PostgreSQL database and going to a Laravel Livewire or Vue view.
Rewrite Product as a dataclass
from dataclasses import dataclass
@dataclass
class ProductDC:
name: str
price: float
qty: int = 0
def stock_value(self) -> float:
return self.price * self.qty
def apply_discount(self, percent: float) -> None:
self.price *= (1 - percent / 100)The @dataclass decorator automatically generates __init__ with fields in order, types, and default values. You also get __repr__ (readable print) and __eq__ (value comparison).
Example:
p1 = ProductDC("Running Shoes", 89.90, 10)
p2 = ProductDC("Running Shoes", 89.90, 10)
print(p1) # ProductDC(name='Running Shoes', price=89.9, qty=10)
print(p1 == p2) # True
p1.apply_discount(15)
print(p1.price) # 76.415Immutable dataclasses with frozen=True
If you do not want the data to be modified after creation (useful for security or to avoid side effects), use frozen=True. You can no longer do p1.price = ... but only create new instances.
@dataclass(frozen=True)
class ImmutableProduct:
name: str
price: float
qty: int
def stock_value(self) -> float:
return self.price * self.qty
# No methods that modify self are allowed!Inheritance with dataclasses
It works, but with some care. The child class must redefine all fields of the parent, or use super() in the call. Here is an extension example:
@dataclass
class ClothingProductDC(ProductDC):
size: str
color: str
def description(self) -> str:
return f"{self.name} - {self.size}, {self.color}"What to do right now: Convert one of your old data-only classes into a dataclass. Compare code length before and after. Add frozen=True if the data should never change after initialization.
In summary — what to do now
- Rewrite an existing piece of code — take a script managing products, orders, or customers and turn it into classes. Even a prototype, start with a base class.
- Apply inheritance only where needed — do not force hierarchies. If two classes share less than 30% of the code, it is probably better to keep them separate and use composition.
- Adopt dataclasses for data containers — every time you write a class with only attributes and no complex logic, use
@dataclass. It saves you from typo errors in __init__ and boilerplate. - Test polymorphism — write a function that accepts an object of base or child type and calls a common method. Verify the behavior changes based on the actual class.
- Measure debugging time saved — note how much time you save when an error is localized to one class instead of hundreds of lines of procedural code. The return on investment is immediate.
We at Meteora Web have built platforms for clients in Southern Italy using these principles. Well-structured code is not an aesthetic luxury: it is maintenance you don't pay for, bugs you don't track down, and scalability that doesn't force you to throw everything away when you grow. You don't need a computer science degree to start — just patience and a real problem to solve.
Sponsored Protocol