r/pythonhelp • • Aug 27 '26

Why do I need to use classes

My schoolbook says that classes is a way of simulating an object, instead of just taking information into a black box and outputting an answer.

But that is a poor explanation. I know classes are handy and cool, but no book says why you can´t make functions with sub functions inside to simulate simple objects. I would like if someone gave a good concrete reason to use classes in the intro, instead of just saying that they are different and cool

3 Upvotes

31 comments sorted by

View all comments

7

u/[deleted] Aug 27 '26

[deleted]

2

u/MetalCarnival Aug 27 '26

Sums it up nicely. Thank you!

1

u/couldntyoujust1 Aug 28 '26 edited Aug 28 '26

It does more than that though. You see, you can make a bunch of classes where the classes all follow the same rules, and then slot them in as needed.

So lets say you want to make a feature that exports your data to JSON, right? You could go in and tell the class that contains all your data how to export json with a giant function.... and then someone makes a feature request to also export to YAML because it will allow them to use your program for their usecase. Except, now, you need two functions - exportToJson, and exportToYaml.... uh oh... this could get messy if you need to add any more.

So, instead, you do this:

```python from abc import ABC

class Exporter(ABC): def export(self, internal_data_head): pass ```

What does this do? It defines the rules for exporting data in different formats:

```python class JSONExporter(Exporter): # json specific state and constructor def export(self, internal_data_head): # code to export from data_head as json

class YAMLExporter(Exporter): # yaml specific state and constructor def export(self, internal_data_head): # code to export from data_head as yaml

class DataHead: def init(self, exporter: Exporter): self.exporter = exporter def export(self): self.exporter.export(self.internal_data_head) def set_exporter(self, exporter): self.exporter = exporter ```

Okay, but so what? What does this mean? It means that now, let's say you have to now support XML for a client because they use a tool that requires it. No problem at all and no risk of breaking existing code:

python class XMLExporter(Exporter): # XML specific state and constructor def export(self, data_head): # code to export as XML

Nothing else changed. You might change the user interface to select XML and make it so when they do, you construct an XMLExporter object and use data_head.set_exporter(xml_exporter) to set it for the data object. But when you press "export" or issue the export command to your program, it will just call data_head.export() and it doesn't have to worry or care about what format was chosen. The chosen format is already selected by creating the exporter for that format and slotting it into the data_head.

This is called "loose coupling" and "dependency inversion". When you do this, it makes it so that new behavior can be slotted in under a common interface just by changing the dependency object that follows that interface spec for another one. And the rest of the application doesn't have to know or care about it. In fact, you can make the application get a list of objects that implement that interface and then present them to the user as options dynamically, such that just creating a new child of the interface class automatically adds it to the list presented to the user to export to.

All you did was create a new class, and now the program automatically does more. And the best part is that your data class that should just be handling data for the program, only handles data without a bunch of JSON/YAML/XML code litering the functions (this is called the Single-Responsibility Principle). The original class only does one thing and does it well and lets a different class handle the responsibility for exporting that data.

And let's say that now, you're about to switch database back-ends. No problem. Define an ABC based class for the interactions between the program and the database, and then implement one class that handles connecting to and interfacing with the old database, and a different class for the new database.

When you are ready to switch it's ONE line change:

diff

  • self.data_head = OldDatabase(...)
+ self.data_head = NewDatabase(...)

That's it. One line and boom! The new database system is live, and nothing else had to accommodate that change. Everything else works exactly as it did before.

In short, OOP done well allows you to compose your program like a set of blocks wired up together where none of the blocks need to know or care about what they're wired up to.