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.
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.
class Expense:
def __init__(self, amount, category):
self.amount = amount
self.category = category
coffee = Expense(4.5, 'food')
print(coffee.amount)
print(coffee.category)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.
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.
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())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.
self out of a method definition and calling it fails in a way that reads oddly at first:
TypeError: label() takes 0 positional arguments but 1 was given
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:
<__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:
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'))food: 4.50
| Special method | Runs when | You use it to |
|---|---|---|
__init__ | You create an object | Set up its starting data |
__str__ | You print the object | Give it a readable form |
__repr__ | You inspect it in the shell | Show 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.
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}')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:
from dataclasses import dataclass
@dataclass
class Expense:
amount: float
category: str
note: str = ''
print(Expense(4.5, 'food'))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.
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.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.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:
Dune has 412 pages
Show solution
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.
class Expense:
currency = 'INR'
def __init__(self, amount):
self.amount = amount
a = Expense(120)
b = Expense(40)
print(a.currency, b.currency)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:
class Cart:
items = []
def add(self, thing):
self.items.append(thing)
a = Cart()
b = Cart()
a.add('pen')
print(b.items)['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.
__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.


DrJha