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.

self_basics.py
# 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.

self Reference to current instance. First parameter of every instance method.

Using self in methods

Access attributes and methods through self.

self_in_methods.py
# 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.

self_vs_class.py
# 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.

class attribute Attribute shared by all instances: defined in class body, not in `__init__`.

Calling other methods

Use self to invoke other methods on the same object.

self_method_calls.py
# 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_mistakes.py
# 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