, ,

Classes and Objects in Python (Python for Beginners, Part 15)

A gentle first look at classes in Python: define a blueprint with __init__ and self, add methods, control how objects print, and know when a class is overkill.

Python for Beginners · Part 15 of 20

Since Part 8 your expenses have been dictionaries, a small bag of keys like amount and category. That works. But every function that touches one has to reach in by key, and nothing ties the data to the things you do with it. A class fixes that. It bundles the data and the behaviour into one named thing you can create many times over.

This is your first look at object oriented programming, the style that organises code around objects. It sounds heavy and it is often taught as if it were. Taken gently, a class is a small, practical idea, and this part keeps it that way.

The gistA class is a blueprint. You define it once with class, give it an __init__ that sets up each new object, and add methods, which are just functions that live on the object and take self as their first parameter. Then you make as many objects from that blueprint as you want, each holding its own data.

This part assumes functions from Part 9 and dictionaries from Part 8, because a class will feel like a tidier version of both. You need Python 3 and about thirty minutes. Read it once, then type the tracker refactor at the end yourself; that is where it clicks.

A class is a blueprint, an object is a thing made from it

Think of a class as the plan for a house and an object as an actual house built from that plan. One plan, many houses, each with its own address and paint colour. In code, the class defines what every expense looks like, and each object is one specific expense with its own amount and category.

python
class Expense:
    def __init__(self, amount, category):
        self.amount = amount
        self.category = category

coffee = Expense(4.5, 'food')
print(coffee.amount)
print(coffee.category)
output
4.5
food

Two new words are doing the work here. __init__ is a special method Python runs automatically every time you make a new object, and its job is to set up that object’s starting data. self is the object being built; self.amount = amount means store this object’s amount. When you write Expense(4.5, 'food'), Python creates a blank object, calls __init__ with it as self, and hands you back the finished object.

class Expense the blueprint amount, category coffee 4.5, food cab 40, travel

Figure 1. One class, many objects. Each object carries its own data.

Adding behaviour with methods

A dictionary can hold the amount and category, but it cannot do anything. A class can, because you add methods: functions defined inside the class that act on the object’s own data. Every method takes self first, so it can reach the object it belongs to.

python
class Expense:
    def __init__(self, amount, category):
        self.amount = amount
        self.category = category

    def label(self):
        return f'{self.category}: {self.amount:.2f}'

coffee = Expense(4.5, 'food')
print(coffee.label())
output
food: 4.50

Notice coffee.label() passes no argument, yet label is defined with self. Python fills self in for you with the object on the left of the dot. That is the whole trick behind methods, and it is also the source of the most common beginner error, which is next.

Gotcha: the forgotten selfLeave self out of a method definition and calling it fails in a way that reads oddly at first:
output
TypeError: label() takes 0 positional
arguments but 1 was given
The one argument it means is the object Python tried to pass as self. Every method needs self as its first parameter, even when you never type an argument at the call. Read the last line, remember it points at a missing self, and the fix is instant.

Printing an object the way you want

Print an object as it comes and you get something unhelpful:

output
<__main__.Expense object at 0x1039c2fd0>

That is Python admitting it has no idea how you want an expense shown. You tell it by adding a __str__ method, another special method, which returns the text to display when the object is printed:

python
class Expense:
    def __init__(self, amount, category):
        self.amount = amount
        self.category = category

    def __str__(self):
        return f'{self.category}: {self.amount:.2f}'

print(Expense(4.5, 'food'))
output
food: 4.50
Special methodRuns whenYou use it to
__init__You create an objectSet up its starting data
__str__You print the objectGive it a readable form
__repr__You inspect it in the shellShow a precise, debug friendly form

Table 1. The three special methods you meet first, and when Python calls each.

The tracker becomes a class

Now refactor the expense tracker. Each expense becomes an Expense object instead of a dictionary, with the receipt label built right into it. The data and the behaviour finally live in one place, and the rest of the program reads more like plain description.

expenses.py
class Expense:
    def __init__(self, amount, category, note=''):
        self.amount = amount
        self.category = category
        self.note = note

    def __str__(self):
        return f'{self.category:<12}{self.amount:>8.2f}'

expenses = [
    Expense(120.5, 'food', 'groceries'),
    Expense(40, 'travel', 'cab'),
]

for e in expenses:
    print(e)

total = sum(e.amount for e in expenses)
print(f'{"Total":<12}{total:>8.2f}')
output
food          120.50
travel         40.00
Total         160.50

Read the loop. print(e) quietly calls the object’s __str__, and e.amount reaches each object’s own number for the total. The note has a default of an empty string, so old code that made an expense without a note still works. This is the same data you have carried since Part 8, now with its formatting attached to it instead of scattered across functions.

When not to reach for a class

Here is the opinion the tutorials skip. A class is not always the right tool, and beginners who just learned them tend to wrap everything in one. If a piece of data has no behaviour, a plain dictionary or a simple function is clearer than a class with nothing but attributes. Reach for a class when data and the actions on it belong together and you have several instances of the same shape, which is exactly the case for expenses.

When your class really is just fields with no logic, Python has a lighter option worth knowing. The dataclass decorator writes the __init__ and a sensible __repr__ for you from a short list of fields:

python
from dataclasses import dataclass

@dataclass
class Expense:
    amount: float
    category: str
    note: str = ''

print(Expense(4.5, 'food'))
output
Expense(amount=4.5, category='food', note='')

Six lines replace the hand written setup, and you still add your own methods when you need them. For data heavy classes this is the version I reach for, and I would rather a beginner learn the plain form first so the shortcut makes sense.

Why this matters on the jobReal codebases are organised around classes: a User, an Order, a Payment. You will read far more classes than you write in your first year, and being comfortable with self, __init__, and methods is what lets you follow that code at all. Knowing when a class is overkill matters just as much, because reviewers notice a plain dictionary dressed up as a class for no reason.
Real interview question"What is self in a Python method?" A clean answer: self is the object the method is being called on, passed in automatically as the first parameter, so the method can read and change that specific object’s data. If you add that this is why every method lists self first even though you do not pass it at the call, you show you understand the mechanism rather than having memorised a rule.
Try it yourselfWrite a class Book with an __init__ that stores a title and pages, and a method summary that returns the title followed by the page count. Create one book and print its summary. Expected output:
expected output
Dune has 412 pages
Show solution
python
class Book:
    def __init__(self, title, pages):
        self.title = title
        self.pages = pages

    def summary(self):
        return f'{self.title} has {self.pages} pages'

book = Book('Dune', 412)
print(book.summary())

Store both values on self in __init__, then read them back with self.title and self.pages inside the method.

Questions beginners have about classes

Why does self appear in the definition but not in the call? Python passes the object in for you. Writing coffee.label() is really Expense.label(coffee) under the hood, and coffee becomes self. You only ever type self in the definition.

What is the difference between a class and an object? The class is the blueprint you define once. An object, also called an instance, is a specific thing built from it. One Expense class, many Expense objects.

Do I have to use double underscores everywhere? No. Only certain special methods like __init__ and __str__ use them, and Python calls those for you. Your own methods and attributes get ordinary names.

Is a method different from a function? A method is just a function that lives on a class and takes self. Same idea as Part 9, attached to an object so it can use that object’s data.

When should I use a dataclass instead? When the class is mostly fields with little behaviour. It writes the boilerplate for you. For anything with real logic, a normal class is fine.

Owned by the object, or shared by the class

Everything you set on self belongs to one object alone. But you can also put a value directly on the class, written outside __init__, and then every object made from that class shares the one value. That is handy for a constant that is the same for all instances, and a genuine trap when the shared thing can change.

python
class Expense:
    currency = 'INR'
    def __init__(self, amount):
        self.amount = amount

a = Expense(120)
b = Expense(40)
print(a.currency, b.currency)
output
INR INR

Here currency lives on the class, so both objects read the same value, while each object keeps its own amount. For a fixed constant that sharing is exactly what you want. The danger shows up when the shared value is a list or a dictionary, because then every object quietly edits the same one:

python
class Cart:
    items = []
    def add(self, thing):
        self.items.append(thing)

a = Cart()
b = Cart()
a.add('pen')
print(b.items)
output
['pen']

Look at that result. The cart b never called add, yet its items already holds the pen, because both carts share the single list defined on the class. The fix is to give each object its own list inside __init__ with self.items = [], so nothing is shared by accident. This is the class version of the mutable default trap you met with functions in Part 9, and it catches people the same way, through output that looks impossible until you see the shared object behind it.

In practiceA shared class attribute is right for constants and wrong for anything that changes per object. When two objects seem to mysteriously affect each other, a mutable value sitting on the class, instead of on each instance, is the first thing to check.
You can nowYou can now define a class, set up objects with __init__ and self, add methods, control how an object prints with __str__, and judge when a class is worth it against a plain dictionary. Your tracker’s expenses are real objects now. In Part 16 you send that data out and pull data in, saving it as JSON and reading a value from a live web API.
class Expenseblueprint: amount, categoryExpense(4.5, ‘food’)Expense(40, ‘travel’)
One class is the blueprint; every object you build from it carries its own data.
Python for Beginners · Part 15 of 20
« Previous: Part 14  |  Complete Guide  |  Next: Part 16

About The Author


Discover more from Journal of Intelligent Infrastructure – By Dr Pranay Jha

Subscribe to get the latest posts sent to your email.

Leave a Reply

Your email address will not be published. Required fields are marked *

Architect’s Toolkit

About the Author

Dr. Pranay Jha is a Cloud and AI Consultant with 18+ years of experience in hybrid cloud, virtualization, and enterprise infrastructure transformation. He specializes in VMware technologies, multi-cloud strategy, and Generative AI solutions. He holds a PhD in Computer Applications with research focused on Cloud and AI, has published multiple research papers, and has been a VMware vExpert since 2016 and a VMUG Community Leader.

Discover more from Journal of Intelligent Infrastructure - By Dr Pranay Jha

Subscribe now to keep reading and get access to the full archive.

Continue reading