Module 4 ended with a clear diagnosis: at Papyrus, data lives in one dictionary (catalog) and behavior lives somewhere else (papyrus_utils.py). final_price() trusts you to pass it the right number; find_book() trusts the dictionary to have the expected shape. Nothing ties the two halves together. Object-oriented programming (OOP) solves exactly that: a class is a mold that packages data and behavior into a single named unit, and every object created from that mold carries its data with it and knows how to operate on it. In this lesson you will build Papyrus's Book class: each book will stop being a dictionary entry and become an object that calculates its own member price.
Contents
- From dictionaries to classes: why make the leap
- Defining a class with
classand__init__ self: the object talks about itself- Instance attributes vs class attributes
- Methods: behavior that travels with the data
- Papyrus's complete
Bookclass - Several objects from the same mold
- Common mistakes and tips
- Exercises with solutions
From dictionaries to classes: why make the leap
Remember how the catalog ended up after the big refactor in 04-03:
And how a price gets calculated, from papyrus_utils.py:
It works, but look at the seams:
- Nothing guarantees the structure: if someone writes
{"price": "12,50"}, no part of the program complains until it blows up somewhere else. - The knowledge is scattered: to find out what you can do with a book you have to dig through
papyrus_utils.py. - The function and the data don't know each other:
final_price()accepts any number; it has no idea it works with books.
A class inverts the relationship: instead of functions that receive data, you have data that carries its functions built in.
| Approach | Data | Behavior | Who guarantees consistency? |
|---|---|---|---|
| Dict + functions (module 4) | catalog["Hamlet"] |
papyrus_utils.final_price(...) |
Nobody: programmer discipline |
| Class (this module) | hamlet.price |
hamlet.member_price() |
The class itself |
Defining a class with class and __init__
The minimal syntax:
class Book:
"""A book in the Papyrus catalog."""
def __init__(self, title, price, stock):
self.title = title
self.price = price
self.stock = stockLine-by-line breakdown:
class Book:— declares the class. By PEP 8 convention, class names use CapWords (Book,EBook), unlike functions and variables (final_price,total_stock).- The docstring works just like it did for functions in 03-01: it documents what the class is for.
__init__is the initializer: a special method that Python runs automatically every time you create an object. It receives the initial data and stores it.self.title = title— creates an instance attribute: a variable that belongs to that specific object, not to the class in general.
To create an object (an instance), you call the class as if it were a function:
odyssey = Book("The Odyssey", 12.50, 4)
hamlet = Book("Hamlet", 9.95, 6)
print(odyssey.title) # The Odyssey
print(hamlet.price) # 9.95
print(odyssey.stock) # 4Notice that you do not pass self when calling: you write Book("The Odyssey", 12.50, 4) with three arguments, and Python places the freshly created object into self for you.
self: the object talks about itself
self is the parameter that receives the object itself — the one currently being worked on. When Ana writes odyssey.member_price(), Python translates it internally into Book.member_price(odyssey): the object travels as the first argument.
class Book:
def __init__(self, title, price, stock):
self.title = title
self.price = price
self.stock = stock
def in_stock(self):
return self.stock > 0- Inside
in_stock,self.stockmeans "the stock of this book". - If
odyssey.stockis 4 andfaust.stockis 0,odyssey.in_stock()returnsTrueandfaust.in_stock()returnsFalse— same code, different data, becauseselfpoints to a different object on each call.
selfis not a reserved word: it is a universal convention. You could name it something else, but no Python programmer ever does. Always respect it.
Instance attributes vs class attributes
The VAT on books and the member discount are the same for every book at Papyrus. It makes no sense to store them in each object: they are class attributes, shared by all instances.
class Book:
BOOK_VAT = 0.04 # class attribute: shared
MEMBER_DISCOUNT = 0.05 # class attribute: shared
def __init__(self, title, price, stock):
self.title = title # instance attributes: each object's own
self.price = price
self.stock = stock| Characteristic | Instance attribute | Class attribute |
|---|---|---|
| Defined in | __init__, with self. |
Class body, without self |
| Belongs to | Each object separately | The class (everyone shares it) |
| Example at Papyrus | title, price, stock |
BOOK_VAT, MEMBER_DISCOUNT |
| Varies between objects | Yes (odyssey.stock ≠ hamlet.stock) |
No (same for all) |
| Access | odyssey.price |
Book.BOOK_VAT or odyssey.BOOK_VAT |
print(Book.BOOK_VAT) # 0.04 — via the class
print(odyssey.BOOK_VAT) # 0.04 — via the instance (inherited from the class)When you access odyssey.BOOK_VAT, Python looks in the object first; if it doesn't find it there, it climbs up to the class. That's why it works from both sides. Notice how the constants that lived loose back in module 1 (STORE_NAME, BOOK_VAT) now find a natural home inside the class they conceptually belong to.
Methods: behavior that travels with the data
A method is a function defined inside a class. Papyrus's pricing formula (base × (1−discount) × (1+VAT), rounded to 2 decimal places) stops living in papyrus_utils.py and becomes knowledge the book itself carries:
def member_price(self):
"""Final price for members: 5% discount and 4% VAT."""
gross = self.price * (1 - Book.MEMBER_DISCOUNT) * (1 + Book.BOOK_VAT)
return round(gross, 2)Compare the two calls:
# Module 4: an external function that receives the data
final_price(catalog["The Odyssey"]["price"], member=True) # 12.35
# Module 5: the object works it out on its own
odyssey.member_price() # 12.35The result is identical (12.35 EUR), but the second style can't grab the wrong data: the method always operates on its own book's price.
Papyrus's complete Book class
class Book:
"""A book in the Papyrus catalog."""
BOOK_VAT = 0.04
MEMBER_DISCOUNT = 0.05
def __init__(self, title, price, stock):
self.title = title
self.price = price
self.stock = stock
def member_price(self):
"""Final price for members, rounded to 2 decimal places."""
gross = self.price * (1 - Book.MEMBER_DISCOUNT) * (1 + Book.BOOK_VAT)
return round(gross, 2)
def in_stock(self):
"""True if at least one copy is left."""
return self.stock > 0And in action, with the four canonical titles:
odyssey = Book("The Odyssey", 12.50, 4)
hamlet = Book("Hamlet", 9.95, 6)
quixote = Book("Don Quixote", 15.90, 8)
faust = Book("Faust", 21.00, 0)
for book in (odyssey, hamlet, quixote, faust):
status = "available" if book.in_stock() else "OUT OF STOCK"
print(f"{book.title:<12} member: {book.member_price():>6.2f} EUR [{status}]")The Odyssey member: 12.35 EUR [available] Hamlet member: 9.83 EUR [available] Don Quixote member: 15.71 EUR [available] Faust member: 20.75 EUR [OUT OF STOCK]
The prices match exactly what Luis (member LUIS-001) has been paying since module 4: the logic is the same — it has just moved house.
Several objects from the same mold
The key distinction of the day: the class is the mold (written once); each instance is an independent object with its own values.
classDiagram
class Book {
+BOOK_VAT = 0.04
+MEMBER_DISCOUNT = 0.05
+title
+price
+stock
+member_price()
+in_stock()
}
Book <.. odyssey : instance
Book <.. hamlet : instance
Book <.. faust : instance
class odyssey {
title = "The Odyssey"
price = 12.50
stock = 4
}
class hamlet {
title = "Hamlet"
price = 9.95
stock = 6
}
class faust {
title = "Faust"
price = 21.00
stock = 0
}
Every object has its own identity: modifying faust.stock doesn't affect odyssey.stock (none of the aliasing traps from 04-01, because they are distinct objects from the moment they are created). You can check the type of any object with type():
One detail you may already have spotted if you've tried print(odyssey):
It comes out "ugly": Python doesn't know how to display a Book as readable text, so it shows its memory address. There's an elegant fix, but it belongs to the magic methods of lesson 05-05. Until then, print specific attributes (book.title) whenever you need readable output.
Common Mistakes and Tips
- Forgetting
selfin the method definition.def member_price():withoutselfraisesTypeError: member_price() takes 0 positional arguments but 1 was givenwhen called, because Python always passes the object as the first argument. Every instance method starts withself. - Forgetting
self.when using an attribute. Inside a method, plainpriceis a nonexistent local variable (NameError); the object's attribute isself.price. Revisit variable scope from 03-01: the rules haven't changed. - Writing
Book()with no arguments when__init__requires them:TypeError: __init__() missing 3 required positional arguments. The parameters of__init__work exactly like those of any function (03-02), including default values:def __init__(self, title, price, stock=0)would allow creating books with no initial stock. - Confusing the class with the instance.
Book.member_price()fails because there's no object to operate on;odyssey.member_price()works. The class is the blueprint; the instance is the building. - Using a mutable class attribute as a per-instance value.
reservations = []in the class body would be shared by every book — the same trap as the mutable default argument in 03-02. Per-object lists and dictionaries are always created in__init__withself.reservations = []. - Tip: start every class by answering two questions: what data does it describe? (attributes) and what does it know how to do with it? (methods). If a method never touches
self, it should probably be a plain function outside the class.
Exercises
Exercise 1: the Member class
Papyrus manages members like Luis (LUIS-001) and Marta (MARTA-002). Create a Member class with instance attributes name and code, and a method greeting() that returns "Hello, Luis (member LUIS-001)". Create the members Luis, Marta (MARTA-002) and Pau (PAU-003) and print all three greetings with a loop.
Exercise 2: extending Book with sales
Add a method sell(units) to the Book class that subtracts units from the stock only if there is enough, returning True if the sale went through and False if it didn't. Test it: Julia wants 2 copies of "The Odyssey" (stock 4) and Omar wants 1 "Faust" (stock 0).
Exercise 3: instance counter with a class attribute
Add a class attribute total_books = 0 to Book that gets incremented by 1 inside __init__ every time a book is created (hint: Book.total_books += 1). Create the four catalog books and verify that Book.total_books equals 4.
Solutions
Solution 1:
class Member:
"""A member of the Papyrus readers' club."""
def __init__(self, name, code):
self.name = name
self.code = code
def greeting(self):
return f"Hello, {self.name} (member {self.code})"
members = [Member("Luis", "LUIS-001"), Member("Marta", "MARTA-002"), Member("Pau", "PAU-003")]
for member in members:
print(member.greeting())Each object stores its own name and code; the greeting() method is the same for all three, but self makes each one speak about itself.
Solution 2:
class Book:
BOOK_VAT = 0.04
MEMBER_DISCOUNT = 0.05
def __init__(self, title, price, stock):
self.title = title
self.price = price
self.stock = stock
def in_stock(self):
return self.stock > 0
def sell(self, units):
"""Deducts stock if there is enough; returns True/False."""
if units <= self.stock:
self.stock -= units
return True
return False
odyssey = Book("The Odyssey", 12.50, 4)
faust = Book("Faust", 21.00, 0)
print(odyssey.sell(2)) # True → Julia takes 2
print(odyssey.stock) # 2
print(faust.sell(1)) # False → Omar misses out on Faust (off to the reservation queue from 04-06)
print(faust.stock) # 0Notice how the check protects the data: the stock can never go negative as long as everyone uses sell(). Making sure nobody can bypass it is precisely the subject of encapsulation (05-04).
Solution 3:
class Book:
total_books = 0 # class attribute: shared counter
def __init__(self, title, price, stock):
self.title = title
self.price = price
self.stock = stock
Book.total_books += 1 # updated through the CLASS, not through self
odyssey = Book("The Odyssey", 12.50, 4)
hamlet = Book("Hamlet", 9.95, 6)
quixote = Book("Don Quixote", 15.90, 8)
faust = Book("Faust", 21.00, 0)
print(Book.total_books) # 4The counter lives in the class, which is why every instance shares it and every creation increments it. If you wrote self.total_books += 1, you'd create an instance attribute that shadows the class one — a classic mistake.
Conclusion
Today you joined together what module 4 left apart: the Book class packages the data (title, price, stock) with its behavior (member_price(), in_stock()) in a single unit. You've learned that class defines the mold, __init__ initializes each object, self lets each instance speak about itself, and that instance attributes (different per object) coexist with class attributes (BOOK_VAT, MEMBER_DISCOUNT, shared by all). Every Papyrus book now calculates its own member price: 12.35 EUR for "The Odyssey", 20.75 EUR for the sold-out "Faust". But Ana's shop doesn't sell only books: there are also magazines and, soon, ebooks with no physical stock. Will we have to copy and paste the Book class three times? Not at all: the next lesson introduces inheritance, the mechanism by which a Product class will share what's common while each kind of item adds only what makes it different.
Python Programming Course
Module 1: Introduction to Python
- Introduction to Python
- Setting Up the Development Environment
- Python Syntax and Basic Data Types
- Variables and Constants
- Basic Input and Output
- Virtual Environments and Package Management
Module 2: Control Structures
Module 3: Functions and Modules
- Defining Functions
- Function Arguments
- Lambda Functions
- Modules and Packages
- Standard Library Overview
Module 4: Data Structures
Module 5: Object-Oriented Programming
Module 6: File Handling
Module 7: Error and Exception Handling
- Introduction to Exceptions
- Handling Exceptions
- Raising Exceptions
- Custom Exceptions
- Best Practices and Error Logging
Module 8: Advanced Topics
- Type Hints
- Decorators
- Generators
- Context Managers
- Concurrency: Threads and Processes
- Asyncio for Asynchronous Programming
Module 9: Testing and Debugging
- Introduction to Testing
- Unit Testing with unittest
- Testing with pytest
- Test-Driven Development
- Debugging Techniques
- Using pdb for Debugging
Module 10: Web Development with Python
- Introduction to Web Development
- Flask Framework Fundamentals
- Building REST APIs with Flask
- Introduction to Django
- Building Web Applications with Django
Module 11: Data Science with Python
- Introduction to Data Science
- NumPy for Numerical Computing
- Pandas for Data Manipulation
- Matplotlib for Data Visualization
- Introduction to Machine Learning with scikit-learn
