What is it? #
A class is a description of a kind of thing. It says what data every instance of that thing holds, and what operations it supports.
Nothing exists yet when you write a class. It is a plan. Creating an actual thing from the plan gives you an object, which is the next lesson.
A well-designed class covers one concept. Product holds product data and product behaviour. It does not also send emails, write to files or render HTML — those are other concerns with their own homes.
The test for whether something belongs in a class is simple: does this operation need the data inside the class? If yes, it probably belongs. If it merely takes the object as an argument and does unrelated work, it belongs elsewhere.
Think of it like this #
A class is an architect's drawing of an apartment. It specifies the rooms, the wiring and the plumbing. Nobody lives in a drawing.
Build from the drawing and you get an apartment someone can occupy — that is an object. One drawing, many apartments, each with its own furniture.
Simple example #
An online shop needs products. Every product has a SKU, a name and a price, and can tell you its price with tax and whether it is in stock. That shared shape and behaviour is the class.
Code #
class Product:
"""A single sellable item in the catalogue."""
TAX_RATE = 0.18 # shared by all products
def __init__(self, sku: str, name: str, price: float, stock: int = 0):
self.sku = sku
self.name = name
self.price = price
self.stock = stock
def price_with_tax(self) -> float:
return round(self.price * (1 + self.TAX_RATE), 2)
def is_available(self, quantity: int = 1) -> bool:
return self.stock >= quantity
def reserve(self, quantity: int) -> None:
if not self.is_available(quantity):
raise ValueError(f"Only {self.stock} left of {self.name}")
self.stock -= quantity
def __repr__(self) -> str:
return f"Product({self.sku!r}, {self.name!r}, {self.price})"
# What does NOT belong in Product
# - def send_low_stock_email(self) -> an email concern
# - def render_product_page(self) -> a presentation concern
# - def save_to_database(self) -> a storage concern
# Each of those is its own class or module that uses Product.
How it works #
class Product: opens the definition. Everything indented under it belongs to the class.
The docstring says what the class represents in one line. If you cannot write that line clearly, the class is probably doing more than one thing.
TAX_RATE is a class attribute — one value shared by every product, referenced as self.TAX_RATE inside methods.
__init__ lists what every product must have. The parameters make the required data obvious at a glance.
price_with_tax, is_available and reserve all use the product's own data. That is what makes them methods rather than free functions.
reserve enforces a rule: you cannot take more than exists. Keeping that check inside the class means no caller can bypass it.
The comment block at the end is the design point. Sending email, rendering HTML and saving to a database each involve other systems. Putting them here would mean the class changes whenever your email provider, template engine or database changes — three unrelated reasons to modify one file. That is exactly what the Single Responsibility Principle warns about.
Real-world use #
Classes map to the nouns in your domain: User, Order, Invoice, Subscription, Vehicle, Booking. When a team talks about "the order", having a class with that name makes conversations and code line up.
In frameworks, classes get specific roles: a model class for data and rules, a repository for storage, a serializer for input and output, a service for workflows. That separation is why a change to storage does not ripple into business rules.
Not everything deserves a class. A module of functions is often better for stateless helpers, formatting and calculations. Reach for a class when data and rules travel together.
Common mistakes #
- Creating a class that does five unrelated things, so every change touches it.
- Putting storage, email or rendering inside a domain class.
- Making a class with no state and one method — that is a function.
- Choosing vague names like Manager, Helper or Handler that say nothing about the concept.
- Exposing every attribute publicly when some should be protected by methods.
Practice #
Write a Vehicle class with a registration number, make, model and current fuel level. Add methods to refuel (rejecting overfilling past capacity), to drive a distance (rejecting it if there is not enough fuel), and a __repr__. Then list three operations that should NOT live inside this class and say where they belong.