Classes, Objects, Attributes and Methods
advanced35 minLearning objectives
- Create classes containing attributes, methods and constructors in Python
Learn
AQA 4.1.2.3 — Building classes properly
Retrieval: the previous lesson introduced a bare-bones Book class — a title, an author, and one method. This lesson builds it out properly, and creates several Book objects whose state changes independently, exactly the property that made OOP worth introducing in the first place.
Key vocabulary
- Instance attribute — an attribute that belongs to one specific object, set (usually) inside
__init__. - Instance method — a function defined inside a class that operates on one specific object, always taking
selfas its first parameter. - Default parameter — a constructor parameter with a fallback value, used when the caller doesn't supply one.
Understand — what self actually is
When you call hobbit.describe(), Python silently rewrites it as Book.describe(hobbit) — self is just the object the method was called on, passed in automatically as the first argument. This is why every instance method needs self as its first parameter: without it, the method would have no way to know which object's attributes it should actually read or change. It's directly analogous to a Sequence 10 function call getting its own stack frame with its own copy of its parameters — here, self is what lets one method body serve every object built from the class, each call operating on its own object's data.
See it — building the Book class up, stage by stage
Stage 1 — attributes and constructor only:
class Book:
def __init__(self, title, author, isbn):
self.title = title
self.author = author
self.isbn = isbn
self.is_available = True # every new book starts available
Stage 2 — add a method that changes state:
class Book:
def __init__(self, title, author, isbn):
self.title = title
self.author = author
self.isbn = isbn
self.is_available = True
def borrow(self):
if not self.is_available:
return f"{self.title} is already on loan."
self.is_available = False
return f"You have borrowed {self.title}."
Stage 3 — add a second method:
class Book:
def __init__(self, title, author, isbn):
self.title = title
self.author = author
self.isbn = isbn
self.is_available = True
def borrow(self):
if not self.is_available:
return f"{self.title} is already on loan."
self.is_available = False
return f"You have borrowed {self.title}."
def return_book(self):
self.is_available = True
return f"Thank you for returning {self.title}."
Trace it — independent objects, independent state
hobbit = Book("The Hobbit", "Tolkien", "978-0261102217")
dune = Book("Dune", "Herbert", "978-0441172719")
print(hobbit.borrow()) # "You have borrowed The Hobbit."
print(dune.is_available) # True - completely unaffected by hobbit's borrow
print(hobbit.borrow()) # "The Hobbit is already on loan."
Borrowing hobbit changes only hobbit's own is_available attribute — dune, built from the identical class, is entirely untouched. This is the direct payoff of bundling data and behaviour: each object genuinely owns and protects its own state.
Debug it — diagnose, explain, fix, test, justify
class Book:
def __init__(self, title, author, isbn):
self.title = title
self.author = author
self.isbn = isbn
self.is_available = True
def borrow():
self.is_available = False
return f"You have borrowed {self.title}."
hobbit = Book("The Hobbit", "Tolkien", "978-0261102217")
print(hobbit.borrow())
This crashes with TypeError: borrow() takes 0 positional arguments but 1 was given.
- Diagnose: compare
def borrow():here against the working version above — what's missing from the method's own definition? - Explain:
hobbit.borrow()is called with no arguments written inside the brackets at all — so where does Python think an argument is coming from? - Fix: correct the method's definition.
- Test: confirm
hobbit.borrow()now works and correctly updatesis_available. - Justify: explain, using the "Understand" section above, exactly what Python was silently trying to pass into
borrow()that had nowhere to go.
(The method definition is missing self as its first parameter. Calling hobbit.borrow() is automatically rewritten by Python as Book.borrow(hobbit) - it silently supplies hobbit as an argument regardless of what's written inside the call's own brackets, so a method defined to take zero parameters immediately receives one it wasn't expecting. Adding self as the first parameter of borrow fixes it.)
Common mistake
Confusing an attribute with a method - writing hobbit.is_available (correct - reads the attribute directly) versus mistakenly writing hobbit.is_available() (incorrect - Python looks for a method called is_available, which doesn't exist, and raises TypeError: 'bool' object is not callable). Attributes are accessed directly; methods are called, with parentheses, precisely because a method is a function and an attribute is a value.
Modify it
Add a renew() method to Book that only makes sense to call while the book is currently on loan (i.e. is_available is False) — it should return a confirmation message, and leave is_available unchanged (renewing doesn't return the book). If called while the book is actually available, it should return a message explaining that renewal doesn't apply.
Design it
Design a new class, Member, from scratch: a library member has a name and a list of borrowed_books (starting empty). Give it a method borrow_book(book) that calls the book's own borrow() method and, if successful, adds the book to the member's borrowed_books list.
Challenge
Extend Member with a method has_reached_limit() that returns True if the member already has 3 or more books in borrowed_books, and modify borrow_book so it refuses to borrow (returning an appropriate message) once the limit is reached.
Looking ahead: the next lesson protects Book's internal state properly (encapsulation), and introduces a second class, EBook, that shares almost everything with Book but not quite everything (inheritance).