What is it? #
A class describes a kind of thing. An object is one of them. Product is a class; the specific ₹499 notebook in your cart is an object.
Objects hold instance attributes — values that belong to that one object. Classes can also hold class attributes, shared by every instance. Mixing those up is a classic source of confusion.
Methods come in three kinds. An instance method takes self and works with one object. A class method takes cls and works with the class itself, usually to build objects in a different way. A static method takes neither and is simply a function that lives inside the class for organisation.
Whatever you do, make it impossible to create an invalid object. Validate in __init__, and later code stops needing defensive checks everywhere.
Think of it like this #
A class is the blueprint for a flat in a building. Every flat has the same layout — that is the class. The furniture inside flat 402 is its own — that is instance state. The building's postal address is shared by all flats — that is a class attribute.
Simple example #
You are modelling a product in a catalogue. Products come from your own code, and also from rows in a CSV import, so you want a second way to build one. You also need a tax helper that does not depend on any particular product.
Code #
class Product:
currency = "INR" # class attribute — shared
def __init__(self, sku, name, price):
if price < 0:
raise ValueError("price cannot be negative")
self.sku = sku # instance attributes — per object
self.name = name
self.price = price
# instance method — works on one product
def discounted(self, percent):
return round(self.price * (1 - percent / 100), 2)
# class method — an alternative constructor
@classmethod
def from_csv_row(cls, row):
sku, name, price = row.split(",")
return cls(sku.strip(), name.strip(), float(price))
# static method — related, but needs no product
@staticmethod
def with_gst(amount, rate=0.18):
return round(amount * (1 + rate), 2)
def __repr__(self):
return f"Product({self.sku!r}, {self.name!r}, {self.price})"
pen = Product("P-1", "Gel Pen", 60)
book = Product.from_csv_row("B-2, Notebook, 499")
print(pen.discounted(10)) # 54.0
print(Product.with_gst(book.price)) # 588.82
print(pen.currency, book.currency) # INR INR
print(book) # Product('B-2', 'Notebook', 499.0)
Product.currency = "USD" # changes it for every product
print(pen.currency) # USD
How it works #
currency = "INR" sits directly in the class body, so it belongs to the class. Every instance can read it, and changing it on the class changes it for all of them — as the last two lines demonstrate.
Everything assigned to self inside __init__ belongs to that one object. pen.price and book.price are independent.
The validation in __init__ is the important habit. A Product with a negative price can never exist, so no other function has to check for it.
discounted is an instance method: it reads self.price, so it needs a specific product.
from_csv_row is a class method. It receives the class as cls and returns cls(...), which means a subclass calling it gets an instance of the subclass rather than of Product. Alternative constructors are the main reason class methods exist.
with_gst needs no product and no class — it is a pure calculation. Marking it @staticmethod says exactly that, and it can be called as Product.with_gst(1000) without creating anything.
__repr__ returns a string that ideally looks like the code to recreate the object. The !r in the f-string applies repr() to each value, which is why the strings keep their quotes.
Real-world use #
Alternative constructors appear constantly in real libraries: datetime.fromtimestamp(), DataFrame.from_dict(), Model.from_json(). When your class can be built from several source formats, class methods keep __init__ simple and each conversion clearly named.
Class attributes are useful for constants and configuration shared by all instances, and dangerous when someone stores mutable state in one by accident — a class-level list is shared by every object, the same trap as a mutable default argument.
The "validate at the edge" rule matters in production. If every object is valid by construction, your business logic can focus on business rules instead of re-checking inputs at every step.
Common mistakes #
- Using a mutable class attribute (like a list) as if it were per-instance. Every object shares it.
- Forgetting
self.when assigning inside__init__, creating a local variable that vanishes. - Building objects in an invalid state and validating later. Validate in
__init__. - Writing a static method that actually needs instance data. It should be an instance method.
- Accessing another object’s private attributes directly instead of going through its methods.
Practice #
Write an Employee class with name, salary and a class attribute company. Add an instance method that returns the monthly pay, a class method from_dict that builds an employee from a dictionary, and a static method that converts an annual figure to monthly. Reject a negative salary in __init__.