AiTechWorlds
AiTechWorlds
Think about a dog. It has things — a name, a breed, an age. It does things — bark, fetch, sit. In OOP:
Good class design separates what something is from what it can do. This lesson goes deeper into both.
You met these briefly before. Let's nail down the distinction with a concrete example:
class Employee:
company = "AiTechWorlds Inc." # CLASS attribute — shared by all employees
employee_count = 0 # CLASS attribute — tracks total
def __init__(self, name, department, salary):
self.name = name # INSTANCE attribute — unique per person
self.department = department
self.salary = salary
Employee.employee_count += 1 # Update class attribute via class name
emp1 = Employee("Alice", "Engineering", 75000)
emp2 = Employee("Bob", "Marketing", 65000)
emp3 = Employee("Carol", "Engineering", 80000)
# Accessing class attributes — same for all
print(emp1.company) # AiTechWorlds Inc.
print(Employee.company) # AiTechWorlds Inc. (same result)
print(Employee.employee_count) # 3
# Accessing instance attributes — unique per object
print(emp1.name) # Alice
print(emp2.name) # Bob
Rule of thumb: If every object of the class shares the same value, it's a class attribute. If each object has its own value, it's an instance attribute.
Python has a system of special methods with double underscores on both sides: __method__. These are called dunder methods (short for "double underscore"). They're how Python integrates your class with built-in behaviors.
__str__ — Making Objects Print NicelyWithout __str__:
class Product:
def __init__(self, name, price):
self.name = name
self.price = price
p = Product("Python Book", 29.99)
print(p) # <__main__.Product object at 0x7f1b2c3d4e5f>
That memory address is useless. Let's fix it:
class Product:
def __init__(self, name, price):
self.name = name
self.price = price
def __str__(self):
return f"{self.name} — ${self.price:.2f}"
p = Product("Python Book", 29.99)
print(p) # Python Book — $29.99
Now print(p) calls __str__ automatically and shows something human-readable.
__repr__ — The Developer View__repr__ is for developers. It should show enough information to recreate the object:
def __repr__(self):
return f"Product(name='{self.name}', price={self.price})"
When you type an object in the Python REPL without print(), __repr__ is called.
__len__ — Making Objects Work with len()class Playlist:
def __init__(self, name):
self.name = name
self.songs = []
def add_song(self, song):
self.songs.append(song)
def __len__(self):
return len(self.songs)
def __str__(self):
return f"Playlist '{self.name}' ({len(self)} songs)"
my_playlist = Playlist("Coding Mix")
my_playlist.add_song("Lo-Fi Beat 1")
my_playlist.add_song("Focus Music")
my_playlist.add_song("Deep Work Vibes")
print(len(my_playlist)) # 3
print(my_playlist) # Playlist 'Coding Mix' (3 songs)
Encapsulation means bundling data and the methods that operate on it, and controlling access from outside. Python doesn't strictly enforce access control like some languages, but it has conventions:
class BankAccount:
def __init__(self, owner, balance):
self.owner = owner
self._balance = balance # Single underscore = "please don't touch directly"
self.__pin = "1234" # Double underscore = name-mangled, harder to access
@property
def balance(self):
"""Read-only access to balance."""
return self._balance
def deposit(self, amount, pin):
if pin != self.__pin:
print("Invalid PIN")
return
if amount > 0:
self._balance += amount
print(f"Deposited ${amount}. Balance: ${self._balance}")
acc = BankAccount("Alice", 1000)
# Reading balance through the property (safe)
print(acc.balance) # 1000
# Depositing requires PIN (controlled)
acc.deposit(500, "1234") # Works
acc.deposit(500, "9999") # Invalid PIN
# Direct access is discouraged but possible
# acc._balance = 999999 # Works but bad practice
The @property decorator turns a method into a read-only attribute. Now acc.balance reads naturally without parentheses.
Not all methods need self. Python offers two alternatives:
class Temperature:
unit = "Celsius" # Class attribute
def __init__(self, value):
self.value = value
def to_fahrenheit(self):
"""Instance method — needs self, accesses instance data."""
return (self.value * 9/5) + 32
@classmethod
def from_fahrenheit(cls, fahrenheit):
"""Class method — receives cls (the class itself), creates an instance."""
celsius = (fahrenheit - 32) * 5/9
return cls(celsius)
@staticmethod
def is_valid(value):
"""Static method — no self or cls needed, just utility logic."""
return value >= -273.15
# Instance method
t1 = Temperature(100)
print(f"{t1.value}°C = {t1.to_fahrenheit()}°F")
# Class method used as an alternative constructor
t2 = Temperature.from_fahrenheit(212)
print(f"Created from 212°F: {t2.value:.1f}°C")
# Static method — called on class, no instance needed
print(Temperature.is_valid(-300)) # False (below absolute zero)
print(Temperature.is_valid(25)) # True
| Method Type | First Parameter | When to Use |
|---|---|---|
| Instance method | self | Needs to access/modify instance data |
| Class method | cls | Alternative constructors, modifying class state |
| Static method | None | Utility function logically related to the class |
class Book:
total_books = 0
def __init__(self, title, author, isbn, pages):
self.title = title
self.author = author
self.isbn = isbn
self.pages = pages
self.available = True
self.borrower = None
Book.total_books += 1
def __str__(self):
status = "Available" if self.available else f"Borrowed by {self.borrower}"
return f'"{self.title}" by {self.author} | {status}'
def __repr__(self):
return f"Book(title='{self.title}', author='{self.author}', isbn='{self.isbn}')"
def checkout(self, borrower_name):
if self.available:
self.available = False
self.borrower = borrower_name
print(f'✓ "{self.title}" checked out to {borrower_name}')
else:
print(f'✗ "{self.title}" is already borrowed by {self.borrower}')
def return_book(self):
if not self.available:
print(f'✓ "{self.title}" returned by {self.borrower}')
self.available = True
self.borrower = None
else:
print(f'"{self.title}" was not checked out')
@classmethod
def get_total_books(cls):
return cls.total_books
@staticmethod
def is_valid_isbn(isbn):
"""ISBN-13 must be exactly 13 digits."""
return len(isbn) == 13 and isbn.isdigit()
# Using the library
book1 = Book("Python Crash Course", "Eric Matthes", "9781593279288", 544)
book2 = Book("Clean Code", "Robert Martin", "9780132350884", 464)
book3 = Book("The Pragmatic Programmer", "Hunt & Thomas", "9780135957059", 352)
print(book1)
print(book2)
print()
book1.checkout("Alice")
book1.checkout("Bob") # Already taken
book2.checkout("Charlie")
print()
print(f"Total books in system: {Book.get_total_books()}")
print()
book1.return_book()
book1.checkout("Bob") # Now available
print()
for book in [book1, book2, book3]:
print(book)
Output:
"Python Crash Course" by Eric Matthes | Available
"Clean Code" by Robert Martin | Available
✓ "Python Crash Course" checked out to Alice
✗ "Python Crash Course" is already borrowed by Alice
✓ "Clean Code" checked out to Charlie
Total books in system: 3
✓ "Python Crash Course" returned by Alice
✓ "Python Crash Course" checked out to Bob
"Python Crash Course" by Eric Matthes | Borrowed by Bob
"Clean Code" by Robert Martin | Borrowed by Charlie
"The Pragmatic Programmer" by Hunt & Thomas | Available
| Feature | Purpose |
|---|---|
| Instance attributes | Unique data per object |
| Class attributes | Shared data across all objects |
__str__ | Human-readable string representation |
__repr__ | Developer-friendly representation |
@property | Read-only (or controlled) attribute access |
@classmethod | Alternative constructors, class-level operations |
@staticmethod | Utility functions tied to the class |
In the next lesson, we tackle debugging — the skill that separates programmers who give up from those who keep going.
Get this course's notes on Telegram!
Free cheat sheets, summaries & practice exercises