PythonBeginner 14 min Lesson 16 of 30

Day 16 — Classes and Objects

Write real classes: instance versus class attributes, instance methods, class methods, static methods, and keeping objects valid from the start.

Python · Lesson 16 of 30
0/30 done(0%)

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 #

PYTHON
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__.

Quick quiz

  1. 1. What is the difference between a class attribute and an instance attribute?

  2. 2. What does a `@classmethod` receive as its first argument?

  3. 3. When is `@staticmethod` appropriate?

  4. 4. Why validate inputs inside `__init__`?

  5. 5. What does `__repr__` do?

Summary

  • Instance attributes belong to one object; class attributes are shared by all.
  • Instance methods take self, class methods take cls, static methods take neither.
  • Class methods are the natural home for alternative constructors.
  • Validate in `__init__` so invalid objects cannot exist.
  • Always write a useful `__repr__` — future you will be debugging with it.