Object-Oriented Basics
self
The Current Object
When you call dog.bark(), how does bark() know which dog? Python passes the
object automatically as the first parameter, called self. It's how methods
access the object's data and other methods.
What self is
The current object instance.
# Understanding self as Current Object
print("=== What is self? ===\n")
class Dog:
def __init__(self, name):
print(f"__init__ called, self is: {self}")
self.name = name
def bark(self):
print(f"bark() called, self is: {self}")
print(f"{self.name} says: Woof!")
# Create two dogs
buddy =
print()
max_dog = Dog("Max")
print(f"\nbuddy is: {buddy}")
print(f"max_dog is: {max_dog}")
# self becomes the object before the dot
print("\n=== self = Object Before the Dot ===")
buddy.bark() # self = buddy
print()
max_dog.bark() # self = max_dog
print("\n=== Manual Call (Don't Do This) ===")
# These are equivalent:
buddy.bark() # Normal way
Dog.bark(buddy) # What Python actually does
print("\n=== Each Object Has Its Own self ===")
class Counter:
def __init__(self, start=0):
self.count = start
def increment(self):
self.count += 1
def display(self):
print(f"This counter ({id(self)}): {self.count}")
c1 = Counter(0)
c2 =
# Each operates on its own count
c1.increment()
c1.increment()
c2.increment()
c1.display() # self = c1
c2.display() # self = c2
print("\n=== self is Just a Name ===")
class Example:
def method1(self):
print(f"self = {self}")
def method2(this):
print(f"this = {this}") # Works! But don't do it
def method3(potato):
print(f"potato = {potato}") # Also works! Very bad style
obj = Example()
obj.method1() # self
obj.method2() # this (same object)
obj.method3() # potato (same object)
print("\nAlways use 'self' - it's Python convention!")
# Understanding self as Current Object
print("=== What is self? ===\n")
class Dog:
def __init__(self, name):
print(f"__init__ called, self is: {self}")
self.name = name
def bark(self):
print(f"bark() called, self is: {self}")
print(f"{self.name} says: Woof!")
# Create two dogs
buddy =
print()
max_dog = Dog("Max")
print(f"\nbuddy is: {buddy}")
print(f"max_dog is: {max_dog}")
# self becomes the object before the dot
print("\n=== self = Object Before the Dot ===")
buddy.bark() # self = buddy
print()
max_dog.bark() # self = max_dog
print("\n=== Manual Call (Don't Do This) ===")
# These are equivalent:
buddy.bark() # Normal way
Dog.bark(buddy) # What Python actually does
print("\n=== Each Object Has Its Own self ===")
class Counter:
def __init__(self, start=0):
self.count = start
def increment(self):
self.count += 1
def display(self):
print(f"This counter ({id(self)}): {self.count}")
c1 = Counter(0)
c2 =
# Each operates on its own count
c1.increment()
c1.increment()
c2.increment()
c1.display() # self = c1
c2.display() # self = c2
print("\n=== self is Just a Name ===")
class Example:
def method1(self):
print(f"self = {self}")
def method2(this):
print(f"this = {this}") # Works! But don't do it
def method3(potato):
print(f"potato = {potato}") # Also works! Very bad style
obj = Example()
obj.method1() # self
obj.method2() # this (same object)
obj.method3() # potato (same object)
print("\nAlways use 'self' - it's Python convention!")
# Understanding self as Current Object
print("=== What is self? ===\n")
class Dog:
def __init__(self, name):
print(f"__init__ called, self is: {self}")
self.name = name
def bark(self):
print(f"bark() called, self is: {self}")
print(f"{self.name} says: Woof!")
# Create two dogs
buddy =
print()
max_dog = Dog("Max")
print(f"\nbuddy is: {buddy}")
print(f"max_dog is: {max_dog}")
# self becomes the object before the dot
print("\n=== self = Object Before the Dot ===")
buddy.bark() # self = buddy
print()
max_dog.bark() # self = max_dog
print("\n=== Manual Call (Don't Do This) ===")
# These are equivalent:
buddy.bark() # Normal way
Dog.bark(buddy) # What Python actually does
print("\n=== Each Object Has Its Own self ===")
class Counter:
def __init__(self, start=0):
self.count = start
def increment(self):
self.count += 1
def display(self):
print(f"This counter ({id(self)}): {self.count}")
c1 = Counter(0)
c2 =
# Each operates on its own count
c1.increment()
c1.increment()
c2.increment()
c1.display() # self = c1
c2.display() # self = c2
print("\n=== self is Just a Name ===")
class Example:
def method1(self):
print(f"self = {self}")
def method2(this):
print(f"this = {this}") # Works! But don't do it
def method3(potato):
print(f"potato = {potato}") # Also works! Very bad style
obj = Example()
obj.method1() # self
obj.method2() # this (same object)
obj.method3() # potato (same object)
print("\nAlways use 'self' - it's Python convention!")
# Understanding self as Current Object
print("=== What is self? ===\n")
class Dog:
def __init__(self, name):
print(f"__init__ called, self is: {self}")
self.name = name
def bark(self):
print(f"bark() called, self is: {self}")
print(f"{self.name} says: Woof!")
# Create two dogs
buddy =
print()
max_dog = Dog("Max")
print(f"\nbuddy is: {buddy}")
print(f"max_dog is: {max_dog}")
# self becomes the object before the dot
print("\n=== self = Object Before the Dot ===")
buddy.bark() # self = buddy
print()
max_dog.bark() # self = max_dog
print("\n=== Manual Call (Don't Do This) ===")
# These are equivalent:
buddy.bark() # Normal way
Dog.bark(buddy) # What Python actually does
print("\n=== Each Object Has Its Own self ===")
class Counter:
def __init__(self, start=0):
self.count = start
def increment(self):
self.count += 1
def display(self):
print(f"This counter ({id(self)}): {self.count}")
c1 = Counter(0)
c2 =
# Each operates on its own count
c1.increment()
c1.increment()
c2.increment()
c1.display() # self = c1
c2.display() # self = c2
print("\n=== self is Just a Name ===")
class Example:
def method1(self):
print(f"self = {self}")
def method2(this):
print(f"this = {this}") # Works! But don't do it
def method3(potato):
print(f"potato = {potato}") # Also works! Very bad style
obj = Example()
obj.method1() # self
obj.method2() # this (same object)
obj.method3() # potato (same object)
print("\nAlways use 'self' - it's Python convention!")
# Understanding self as Current Object
print("=== What is self? ===\n")
class Dog:
def __init__(self, name):
print(f"__init__ called, self is: {self}")
self.name = name
def bark(self):
print(f"bark() called, self is: {self}")
print(f"{self.name} says: Woof!")
# Create two dogs
buddy =
print()
max_dog = Dog("Max")
print(f"\nbuddy is: {buddy}")
print(f"max_dog is: {max_dog}")
# self becomes the object before the dot
print("\n=== self = Object Before the Dot ===")
buddy.bark() # self = buddy
print()
max_dog.bark() # self = max_dog
print("\n=== Manual Call (Don't Do This) ===")
# These are equivalent:
buddy.bark() # Normal way
Dog.bark(buddy) # What Python actually does
print("\n=== Each Object Has Its Own self ===")
class Counter:
def __init__(self, start=0):
self.count = start
def increment(self):
self.count += 1
def display(self):
print(f"This counter ({id(self)}): {self.count}")
c1 = Counter(0)
c2 =
# Each operates on its own count
c1.increment()
c1.increment()
c2.increment()
c1.display() # self = c1
c2.display() # self = c2
print("\n=== self is Just a Name ===")
class Example:
def method1(self):
print(f"self = {self}")
def method2(this):
print(f"this = {this}") # Works! But don't do it
def method3(potato):
print(f"potato = {potato}") # Also works! Very bad style
obj = Example()
obj.method1() # self
obj.method2() # this (same object)
obj.method3() # potato (same object)
print("\nAlways use 'self' - it's Python convention!")
# Understanding self as Current Object
print("=== What is self? ===\n")
class Dog:
def __init__(self, name):
print(f"__init__ called, self is: {self}")
self.name = name
def bark(self):
print(f"bark() called, self is: {self}")
print(f"{self.name} says: Woof!")
# Create two dogs
buddy =
print()
max_dog = Dog("Max")
print(f"\nbuddy is: {buddy}")
print(f"max_dog is: {max_dog}")
# self becomes the object before the dot
print("\n=== self = Object Before the Dot ===")
buddy.bark() # self = buddy
print()
max_dog.bark() # self = max_dog
print("\n=== Manual Call (Don't Do This) ===")
# These are equivalent:
buddy.bark() # Normal way
Dog.bark(buddy) # What Python actually does
print("\n=== Each Object Has Its Own self ===")
class Counter:
def __init__(self, start=0):
self.count = start
def increment(self):
self.count += 1
def display(self):
print(f"This counter ({id(self)}): {self.count}")
c1 = Counter(0)
c2 =
# Each operates on its own count
c1.increment()
c1.increment()
c2.increment()
c1.display() # self = c1
c2.display() # self = c2
print("\n=== self is Just a Name ===")
class Example:
def method1(self):
print(f"self = {self}")
def method2(this):
print(f"this = {this}") # Works! But don't do it
def method3(potato):
print(f"potato = {potato}") # Also works! Very bad style
obj = Example()
obj.method1() # self
obj.method2() # this (same object)
obj.method3() # potato (same object)
print("\nAlways use 'self' - it's Python convention!")
self refers to the object the method was called on. Python passes it automatically.
Using self in methods
Access attributes and methods through self.
# Using self to Access Attributes and Methods
print("=== Accessing Attributes via self ===\n")
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def introduce(self):
# Access attributes with self.
print(f"Hi, I'm {self.name} and I'm {self.age} years old.")
def have_birthday(self):
self.age += 1
print(f"Happy birthday {self.name}! Now {self.age} years old.")
def compare_age(self, other):
if self.age > other.age:
return f"{self.name} is older than {other.name}"
elif self.age < other.age:
return f"{self.name} is younger than {other.name}"
else:
return f"{self.name} and {other.name} are the same age"
alice = Person("Alice", 30)
bob = Person("Bob", 25)
alice.introduce()
bob.introduce()
alice.have_birthday()
print(alice.compare_age(bob))
print("\n=== Accessing Multiple Attributes ===")
class Rectangle:
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
def perimeter(self):
return 2 * (self.width + self.height)
def is_square(self):
return self.width == self.height
def resize(self, factor):
self.width *= factor
self.height *= factor
def describe(self):
shape = "square" if self.is_square() else "rectangle"
return f"{shape} {self.width}x{self.height}, area={self.area()}"
rect = Rectangle(4, 4)
print(rect.describe())
rect.resize(2)
print(rect.describe())
print("\n=== self in Chained Operations ===")
class BankAccount:
def __init__(self, balance=0):
self.balance = balance
self.transactions = []
def deposit(self, amount):
self.balance += amount
self.transactions.append(f"+{amount}")
return self
def withdraw(self, amount):
if amount <= self.balance:
self.balance -= amount
self.transactions.append(f"-{amount}")
return self
def display(self):
print(f"Balance: ${self.balance}")
print(f"Transactions: {self.transactions}")
return self
# Chain method calls
account = BankAccount(100)
account.deposit(50).deposit(25).withdraw(30).display()
print("\n=== self Refers to Same Object ===")
class IdentityChecker:
def __init__(self, name):
self.name = name
def check_identity(self, other_obj):
# Compare if self and other_obj are same object
if self is other_obj:
print(f"self IS the same object as {other_obj.name}")
else:
print(f"self is DIFFERENT from {other_obj.name}")
obj1 = IdentityChecker("Object1")
obj2 = IdentityChecker("Object2")
obj1.check_identity(obj1) # Same object
obj1.check_identity(obj2) # Different objects
print("\n=== self in String Methods ===")
class Book:
def __init__(self, title, author, pages):
self.title = title
self.author = author
self.pages = pages
def __str__(self):
return f"'{self.title}' by {self.author}"
def __repr__(self):
return f"Book({self.title!r}, {self.author!r}, {self.pages})"
book = Book("Python Guide", "John Doe", 300)
print(book) # Calls __str__
print(repr(book)) # Calls __repr__
self.name accesses attribute. self.other_method() calls another method.
self vs class attributes
Instance attributes vs class-level attributes.
# Instance (self) vs Class Attributes
print("=== Class Attributes ===\n")
class Dog:
species = "Canis familiaris"
count = 0
def __init__(self, name):
self.name = name
Dog.count += 1
# Create dogs
buddy = Dog("Buddy")
max_dog = Dog("Max")
rex = Dog("Rex")
# Class attribute - shared
print(f"Dog.species: {Dog.species}")
print(f"buddy.species: {buddy.species}") # Access via instance
print(f"max_dog.species: {max_dog.species}") # Same value
# Instance attribute - unique
print(f"\nbuddyname: {buddy.name}")
print(f"max_dog.name: {max_dog.name}")
# Count is shared
print(f"\nTotal dogs created: {Dog.count}")
print("\n=== Modifying Class vs Instance ===")
class Cat:
species = "Felis catus"
def __init__(self, name):
self.name = name
whiskers = Cat("Whiskers")
luna = Cat("Luna")
print(f"Before: whiskers.species = {whiskers.species}")
print(f"Before: luna.species = {luna.species}")
# Modify class attribute
Cat.species = "Felis silvestris catus"
print(f"\nAfter Cat.species change:")
print(f"whiskers.species = {whiskers.species}") # Changed!
print(f"luna.species = {luna.species}") # Changed!
# Modify on instance - creates instance attribute!
whiskers.species = "Special Cat"
print(f"\nAfter whiskers.species = 'Special Cat':")
print(f"whiskers.species = {whiskers.species}") # Instance attr
print(f"luna.species = {luna.species}") # Still class attr
print(f"Cat.species = {Cat.species}") # Class attr unchanged
print("\n=== self vs Class in Methods ===")
class Counter:
total = 0 # Class attribute
def __init__(self, name):
self.name = name # Instance attribute
self.count = 0 # Instance attribute
Counter.total += 1
def increment(self):
self.count += 1
Counter.total += 1 # Or self.__class__.total
def display(self):
print(f"{self.name}: count={self.count}, total={Counter.total}")
c1 = Counter("Counter1")
c2 = Counter("Counter2")
c1.increment()
c1.increment()
c2.increment()
c1.display()
c2.display()
print("\n=== When to Use Each ===")
class Player:
# Class attributes: shared by all
max_level = 100
all_players = []
def __init__(self, name, level=1):
# Instance attributes: unique to each
self.name = name
self.level = level
Player.all_players.append(self)
def level_up(self):
if self.level < Player.max_level:
self.level += 1
@classmethod
def get_player_count(cls):
return len(cls.all_players)
p1 = Player("Alice", 10)
p2 = Player("Bob", 5)
print(f"Max level (class): {Player.max_level}")
print(f"Alice's level (instance): {p1.level}")
print(f"Bob's level (instance): {p2.level}")
print(f"Total players: {Player.get_player_count()}")
p1.level_up()
print(f"After level up - Alice: {p1.level}, Bob: {p2.level}")
print("\n=== Summary ===")
print("""
Class Attribute:
- Defined in class body (not __init__)
- Shared by ALL instances
- Access: ClassName.attr or self.attr
- Modify: Use ClassName.attr
Instance Attribute:
- Defined with self.attr = value
- Unique to EACH instance
- Access: self.attr
- Modify: self.attr = new_value
""")
self.x is per-instance. ClassName.x or bare x in class body is shared.
Calling other methods
Use self to invoke other methods on the same object.
# Using self to Call Other Methods
print("=== Calling Methods via self ===\n")
class Calculator:
def __init__(self, value=0):
self.value = value
def add(self, n):
self.value += n
return self
def subtract(self, n):
self.value -= n
return self
def multiply(self, n):
self.value *= n
return self
def square(self):
self.multiply(self.value) # Call self.multiply()
return self
def double(self):
self.multiply(2)
return self
calc = Calculator(5)
calc.square() # 5 * 5 = 25
print(f"5 squared = {calc.value}")
calc = Calculator(3)
calc.double().double() # 3 * 2 * 2 = 12
print(f"3 doubled twice = {calc.value}")
print("\n=== Helper Methods ===")
class ShoppingCart:
def __init__(self):
self.items = []
def add_item(self, name, price, qty=1):
self.items.append({"name": name, "price": price, "qty": qty})
self._log_action(f"Added {qty}x {name}")
def remove_item(self, name):
for item in self.items:
if item["name"] == name:
self.items.remove(item)
self._log_action(f"Removed {name}")
return
self._log_action(f"Not found: {name}")
def _log_action(self, message):
print(f"[Cart] {message}")
def _calculate_subtotal(self):
return sum(i["price"] * i["qty"] for i in self.items)
def get_total(self):
subtotal = self._calculate_subtotal()
return subtotal
cart = ShoppingCart()
cart.add_item("Apple", 1.50, 3)
cart.add_item("Bread", 2.50)
cart.remove_item("Milk") # Not found
print(f"Total: ${cart.get_total():.2f}")
print("\n=== Validation Methods ===")
class User:
def __init__(self, username, email, age):
self.username = username
self.email = email
self.age = age
def _is_valid_username(self):
return len(self.username) >= 3
def _is_valid_email(self):
return "@" in self.email and "." in self.email
def _is_valid_age(self):
return 0 <= self.age <= 150
def is_valid(self):
return (self._is_valid_username() and
self._is_valid_email() and
self._is_valid_age())
def validation_errors(self):
errors = []
if not self._is_valid_username():
errors.append("Username too short")
if not self._is_valid_email():
errors.append("Invalid email format")
if not self._is_valid_age():
errors.append("Invalid age")
return errors
user1 = User("alice", "alice@email.com", 25)
user2 = User("ab", "not-an-email", 200)
print(f"User1 valid: {user1.is_valid()}")
print(f"User2 valid: {user2.is_valid()}")
print(f"User2 errors: {user2.validation_errors()}")
print("\n=== State Management ===")
class Player:
def __init__(self, name, health=100):
self.name = name
self.health = health
self.alive = True
def take_damage(self, amount):
self.health -= amount
print(f"{self.name} takes {amount} damage! Health: {self.health}")
self._check_death()
def heal(self, amount):
if self.alive:
self.health = min(100, self.health + amount)
print(f"{self.name} heals {amount}! Health: {self.health}")
def _check_death(self):
if self.health <= 0 and self.alive:
self.alive = False
self._on_death()
def _on_death(self):
print(f"{self.name} has fallen!")
hero = Player("Hero")
hero.take_damage(30)
hero.take_damage(50)
hero.take_damage(30) # Dies
hero.heal(20) # Can't heal when dead
print("\n=== Method Delegation ===")
class Report:
def __init__(self, title, data):
self.title = title
self.data = data
def generate(self, format="text"):
if format == "text":
return self._generate_text()
elif format == "html":
return self._generate_html()
else:
return self._generate_text()
def _generate_text(self):
lines = [f"=== {self.title} ==="]
for key, value in self.data.items():
lines.append(f"{key}: {value}")
return "\n".join(lines)
def _generate_html(self):
html = [f"<h1>{self.title}</h1>", "<ul>"]
for key, value in self.data.items():
html.append(f" <li>{key}: {value}</li>")
html.append("</ul>")
return "\n".join(html)
report = Report("Sales", {"Q1": 1000, "Q2": 1500, "Q3": 1200})
print(report.generate("text"))
print()
print(report.generate("html"))
self.helper() calls the object's own method. Useful for code organization.
Common mistakes
Errors beginners make with self.
# Common self-Related Mistakes
print("=== Mistake 1: Forgetting self Parameter ===\n")
class WrongCalculator:
def __init__(self, value):
self.value = value
# WRONG: Missing self!
# def add(n):
# self.value += n # NameError: self not defined!
# CORRECT:
def add(self, n):
self.value += n
calc = WrongCalculator(10)
calc.add(5)
print(f"Value: {calc.value}")
print("\n=== Mistake 2: Forgetting self. for Attributes ===")
class WrongPerson:
def __init__(self, name):
self.name = name
def greet_wrong(self):
# WRONG: name without self.
# print(f"Hello, I'm {name}") # NameError: name not defined
pass
def greet_correct(self):
# CORRECT:
print(f"Hello, I'm {self.name}")
person = WrongPerson("Alice")
person.greet_correct()
print("\n=== Mistake 3: Using self Outside Methods ===")
class WrongCounter:
# WRONG: Can't use self here!
# count = self.something # NameError!
# Class attributes don't use self
count = 0
def __init__(self):
# Instance attributes DO use self
self.value = 0
print("Class attribute (no self):", WrongCounter.count)
print("\n=== Mistake 4: Calling Method Without self. ===")
class WrongHelper:
def __init__(self):
self.data = []
def process(self, item):
# WRONG: Calling without self.
# validate(item) # NameError: validate not defined
# CORRECT:
self._validate(item)
self.data.append(item)
def _validate(self, item):
if item is None:
raise ValueError("Item cannot be None")
helper = WrongHelper()
helper.process("test")
print(f"Data: {helper.data}")
print("\n=== Mistake 5: Confusing self and cls ===")
class ConfusingExample:
class_data = []
def __init__(self, value):
self.value = value
# Instance method uses self
def add_to_instance(self, item):
# self refers to THIS object
pass
# Class method uses cls
@classmethod
def add_to_class(cls, item):
# cls refers to the CLASS
cls.class_data.append(item)
# Static method uses neither
@staticmethod
def utility_function(x):
return x * 2
ConfusingExample.add_to_class("item1")
print(f"Class data: {ConfusingExample.class_data}")
print(f"Static result: {ConfusingExample.utility_function(5)}")
print("\n=== Mistake 6: Shadowing with Local Variable ===")
class WrongAssignment:
def __init__(self, name):
self.name = name
def change_name_wrong(self, new_name):
# WRONG: Creates local variable, not instance attr
name = new_name # This is a LOCAL variable!
print(f"Inside method: name = {name}")
print(f"self.name unchanged: {self.name}")
def change_name_correct(self, new_name):
# CORRECT:
self.name = new_name
print(f"self.name changed: {self.name}")
obj = WrongAssignment("original")
print(f"Before: {obj.name}")
obj.change_name_wrong("attempted")
print(f"After wrong: {obj.name}") # Still "original"
obj.change_name_correct("successful")
print(f"After correct: {obj.name}") # Changed!
print("\n=== Mistake 7: Returning Wrong Thing ===")
class WrongReturn:
def __init__(self, items):
self._items = items
def get_items_wrong(self):
# WRONG: Returns the internal list directly
return self._items # Caller can modify our data!
def get_items_correct(self):
# CORRECT: Return a copy
return self._items.copy()
obj = WrongReturn([1, 2, 3])
wrong_items = obj.get_items_wrong()
wrong_items.append(999) # Modifies original!
print(f"After wrong get: {obj._items}") # Has 999!
obj2 = WrongReturn([1, 2, 3])
correct_items = obj2.get_items_correct()
correct_items.append(999) # Only modifies copy
print(f"After correct get: {obj2._items}") # No 999
print("\n=== Summary of Common Mistakes ===")
print("""
1. Missing 'self' in method parameter
WRONG: def method(x):
RIGHT: def method(self, x):
2. Missing 'self.' when accessing attributes
WRONG: print(name)
RIGHT: print(self.name)
3. Using 'self' outside methods
WRONG: class X: data = self.something
RIGHT: class X: data = 0 # Class attr, no self
4. Calling methods without 'self.'
WRONG: helper_method()
RIGHT: self.helper_method()
5. Creating local variable instead of attribute
WRONG: name = value # Local variable
RIGHT: self.name = value # Instance attribute
""")
Forgetting self in method signature. Forgetting self when accessing attributes.
Exercise: practical.py
Build a class demonstrating proper use of self