| layout | default |
|---|---|
| title | Object-Oriented Python |
| parent | Lessons |
| nav_order | 5 |
| permalink | /lessons/object-oriented-python/ |
| course_lesson | true |
| course_index | 05 |
| previous_page | /lessons/decorators-context-managers/ |
| previous_title | Decorators and Context Managers |
| next_page | /lessons/dataclasses-data-model/ |
| next_title | Dataclasses and Data Model |
Classes group data and behavior. Use them when an object has a clear identity and responsibilities.
A class is a blueprint for a kind of object. An instance is one real object made from that blueprint. A Student class describes what every student object can know and do; Asha and Ravi are separate instances with separate data.
class BankAccount:
def __init__(self, owner, balance=0):
self.owner = owner
self.balance = balance
def deposit(self, amount):
if amount <= 0:
raise ValueError("Deposit must be positive")
self.balance += amount
account = BankAccount("Asha", 500)
account.deposit(200)
print(account.balance)BankAccount("Asha", 500) creates an instance. Python passes that new instance to __init__() as self. Each account receives its own owner and balance attributes.
Calling account.deposit(200) is roughly equivalent to BankAccount.deposit(account, 200). Python supplies self automatically in the normal method call.
class Student:
school_name = "River School" # shared class attribute
def __init__(self, name):
self.name = name # separate instance attributeUse class attributes for values genuinely shared by every instance. Do not use a mutable class attribute such as students = [] for per-instance data.
- An instance method receives
selfand works with one object. - A class method receives
clsand often creates an object in an alternative way. - A static method receives neither automatically and belongs in the class only when it is closely related.
class Temperature:
def __init__(self, celsius):
self.celsius = celsius
@classmethod
def from_fahrenheit(cls, fahrenheit):
celsius = (fahrenheit - 32) * 5 / 9
return cls(celsius)
@staticmethod
def is_valid(celsius):
return celsius >= -273.15Encapsulation means keeping state changes and the rules protecting them together. It is not about hiding everything.
A leading underscore communicates “internal use” by convention:
class BankAccount:
def __init__(self, owner, balance=0):
self.owner = owner
self._balance = balance
@property
def balance(self):
return self._balancePython does not make _balance truly private. Callers cooperate with the convention.
A property can expose a computed value like an attribute:
class Temperature:
def __init__(self, celsius):
self.celsius = celsius
@property
def fahrenheit(self):
return self.celsius * 9 / 5 + 32Do not write a property only to imitate simple direct assignment. Use it when computation, validation, or compatibility makes it valuable.
class Product:
def __init__(self, price):
self.price = price
@property
def price(self):
return self._price
@price.setter
def price(self, value):
if value < 0:
raise ValueError("Price cannot be negative")
self._price = valueThe initializer uses the property setter, so the same rule protects initial and later values.
Inheritance expresses an “is a” relationship. Composition combines objects and is often easier to change.
class Employee:
def __init__(self, name):
self.name = name
def describe(self):
return self.name
class Developer(Employee):
def __init__(self, name, language):
super().__init__(name)
self.language = language
def describe(self):
return f"{super().describe()} writes {self.language}"Use inheritance when the child can safely be used wherever the parent is expected. Deep inheritance trees make behavior difficult to follow.
class EmailSender:
def send(self, message):
print(message)
class NotificationService:
def __init__(self, sender):
self.sender = sender
def notify(self, message):
self.sender.send(message)The service has a sender. Passing the sender into the initializer is dependency injection. Tests can supply a small fake sender without sending real email.
class FakeSender:
def __init__(self):
self.messages = []
def send(self, message):
self.messages.append(message)Python often uses duck typing: if an object provides the needed send() method, its concrete class does not matter.
__repr__() should produce a developer-friendly representation:
class Book:
def __init__(self, title):
self.title = title
def __repr__(self):
return f"Book(title={self.title!r})"!r uses the representation of the value, making quotes and special characters visible.
class Cart:
items = []
def add(self, item):
self.items.append(item)class Developer(Employee):
def __init__(self, name, language):
self.language = languageaccount.balance = -1000Show Bug Hunter fixes
- Create
self.items = []inside__init__()so each cart owns a list. - Call
super().__init__(name)before initializing child-specific state. - Keep the balance in
_balanceand expose controlled deposit/withdraw methods or a validated property.
- Create a
Studentclass. - Add a method that validates a mark.
- Create a bank account with deposit and withdrawal.
- Add a property for a computed value.
- Compare inheritance and composition for notifications.
- Create a base
Shapeclass and two implementations. - Define a useful
__repr__. - Prevent invalid state inside a method.
- Inject a fake sender for testing.
- Refactor a procedural program into classes.
Show hints
- Store the student's name and marks.
- Reject values outside the valid range.
- Check balance before withdrawal.
- Use
@propertyfor read-only calculated data. - Ask whether the relationship is “is a” or “uses”.
- Give each shape an
area()method. - Return a short useful description.
- Do not let callers directly bypass the rule.
- Pass an object with the same
send()method. - Keep each class small and focused.
Show solution ideas
- Define
__init__and a method such asaverage(). - Raise
ValueErrorfor invalid marks. - Change balance only after checking the amount.
@property def total(self): ....- Composition makes the sender replaceable.
- Use a common interface and different formulas.
return f"Student(name={self.name!r})".- Keep state changes behind methods.
- Use a fake object in the test.
- Move related state and behavior together, not every function into a class.
Build a library domain model with Book, Member, and Library classes, validation, and tests.
Explain instance, class, method, property, inheritance, composition, and dependency injection.