self, Methods and Instance State

self is the object the method was called on. Python passes it automatically, and every confusing method error traces back to that one mechanism.

What self actually is

class Counter:
    def __init__(self):
        self.count = 0

    def increment(self):
        self.count += 1
        return self.count


c = Counter()
c.increment()

# What Python actually does:
Counter.increment(c)

print(c.count)          # 2 - both calls worked

c.increment() is shorthand for Counter.increment(c). The object before the dot becomes the first parameter. That is the entire mechanism, and it explains everything below.

c = Counter()

print(Counter.increment)     # <function Counter.increment>  - a plain function
print(c.increment)           # <bound method Counter.increment of ...>
print(c.increment.__self__ is c)      # True - the object it is bound to

The name self is a convention

class Counter:
    def increment(this):     # legal, and nobody writes it this way
        this.count += 1

Python only cares that the first parameter exists. Every Python programmer expects it to be called self, so use that name.

The two errors everyone meets

class Broken:
    def greet():                      # no self
        return "hello"


b = Broken()
# b.greet()
# TypeError: greet() takes 0 positional arguments but 1 was given

The object was passed as the first argument, and the method declared no parameter to receive it. Add self.

class Broken:
    def __init__(self):
        count = 0                     # a LOCAL variable, discarded immediately

    def increment(self):
        self.count += 1               # AttributeError: no attribute 'count'

Without self., the assignment creates a local name inside __init__ that disappears when the method returns. Every attribute needs the prefix.

Methods calling methods

class Order:
    TAX_RATE = 0.18

    def __init__(self, items):
        self.items = items

    def subtotal(self):
        return sum(price * qty for price, qty in self.items)

    def tax(self):
        return self.subtotal() * self.TAX_RATE       # self, even for a class attribute

    def total(self):
        return self.subtotal() + self.tax()

    def summary(self):
        return (
            f"subtotal {self.subtotal():,.2f}  "
            f"tax {self.tax():,.2f}  "
            f"total {self.total():,.2f}"
        )


order = Order([(100, 2), (50, 3)])
print(order.summary())

A method reaches another method through self, never by bare name. Writing subtotal() inside tax would be a NameError.

Methods that return self

class QueryBuilder:
    def __init__(self, table):
        self.table = table
        self.conditions = []
        self.ordering = None

    def where(self, condition):
        self.conditions.append(condition)
        return self                    # allows chaining

    def order_by(self, column):
        self.ordering = column
        return self

    def build(self):
        query = f"SELECT * FROM {self.table}"
        if self.conditions:
            query += " WHERE " + " AND ".join(self.conditions)
        if self.ordering:
            query += f" ORDER BY {self.ordering}"
        return query


print(
    QueryBuilder("notes")
    .where("status = 'published'")
    .where("views > 100")
    .order_by("created_at")
    .build()
)

Instance state and encapsulation by convention

class Account:
    def __init__(self, owner, balance=0):
        self.owner = owner            # public
        self._balance = balance       # internal by convention
        self.__pin = "1234"           # name mangled

    def deposit(self, amount):
        if amount <= 0:
            raise ValueError("deposit must be positive")
        self._balance += amount
        return self._balance

    def withdraw(self, amount):
        if amount > self._balance:
            raise ValueError("insufficient funds")
        self._balance -= amount
        return self._balance

    def get_balance(self):
        return self._balance


account = Account("Meera", 1000)
print(account._balance)          # accessible - Python does not enforce privacy
# print(account.__pin)           # AttributeError
print(account._Account__pin)     # 1234 - the mangled name
NameMeansEnforced?
namePublic. Use it freely.-
_nameInternal. Do not rely on it.No, convention only
__nameName mangled to _Class__namePartly - it avoids collisions, not access

Python has no private keyword. A single underscore is a message to other programmers; the double underscore exists mainly to stop a subclass from accidentally overwriting an attribute of the same name.

Comparing objects

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y


a = Point(1, 2)
b = Point(1, 2)

print(a == b)          # False - two different objects
print(a is b)          # False


class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __eq__(self, other):
        if not isinstance(other, Point):
            return NotImplemented
        return (self.x, self.y) == (other.x, other.y)

    def __hash__(self):                       # needed to stay usable in sets
        return hash((self.x, self.y))


print(Point(1, 2) == Point(1, 2))             # True
print(len({Point(1, 2), Point(1, 2)}))        # 1
Defining __eq__ sets __hash__ to None, making instances unhashable. If your objects should work in sets or as dictionary keys, define __hash__ as well - and only over attributes that never change.

Objects holding objects

class Engine:
    def __init__(self, horsepower):
        self.horsepower = horsepower

    def start(self):
        return f"engine with {self.horsepower}hp started"


class Car:
    def __init__(self, model, horsepower):
        self.model = model
        self.engine = Engine(horsepower)      # a Car HAS an Engine

    def start(self):
        return f"{self.model}: {self.engine.start()}"


print(Car("Sedan", 120).start())

This is composition: building behaviour by holding other objects. It is usually a better first choice than inheritance, and the advanced OOP note explains why.

A worked example

class Library:
    def __init__(self, name):
        self.name = name
        self.books = {}
        self.borrowed = {}

    def add_book(self, isbn, title, copies=1):
        if isbn in self.books:
            self.books[isbn]["copies"] += copies
        else:
            self.books[isbn] = {"title": title, "copies": copies}
        return self

    def available(self, isbn):
        if isbn not in self.books:
            return 0
        return self.books[isbn]["copies"] - len(self.borrowed.get(isbn, []))

    def borrow(self, isbn, member):
        if self.available(isbn) <= 0:
            raise ValueError(f"no copies of {isbn} available")
        self.borrowed.setdefault(isbn, []).append(member)
        return self.books[isbn]["title"]

    def return_book(self, isbn, member):
        holders = self.borrowed.get(isbn, [])
        if member not in holders:
            raise ValueError(f"{member} does not hold {isbn}")
        holders.remove(member)

    def report(self):
        for isbn, details in sorted(self.books.items()):
            free = self.available(isbn)
            print(f"{details['title']:<24}{free}/{details['copies']} available")


library = Library("City Library")
library.add_book("978-1", "Dune", 2).add_book("978-2", "Emma")

library.borrow("978-1", "meera")
library.borrow("978-1", "arun")
library.report()

try:
    library.borrow("978-1", "sara")
except ValueError as error:
    print("error:", error)

Common mistakes

  • Omitting self from a method definition.
  • Omitting self. when assigning an attribute.
  • Calling another method without self..
  • Passing self explicitly at the call site: obj.method(obj).
  • Defining __eq__ without __hash__ and then using the object in a set.
  • Assuming a double underscore makes an attribute private.

Best practices

  • Always name the first parameter self.
  • Reach every attribute and method through self.
  • Mark internals with a single leading underscore.
  • Return self from mutating methods only when chaining genuinely reads better.
  • Validate in the method that changes state, so an object cannot enter an invalid state.

Practice

  1. Write a Stack class with push, pop, peek and is_empty, raising on an empty pop.
  2. Write a class whose methods chain, and build an object in one expression.
  3. Show the exact error for a method missing self, and explain the message.
  4. Give a class value based equality and prove two equal instances collapse in a set.
  5. Model a Playlist that holds Song objects and reports total duration.

Conclusion

obj.method(args) is Class.method(obj, args). Once that substitution is automatic in your head, self stops being mysterious and the two classic errors - a missing self parameter and a missing self. prefix - become instantly readable.

Written by Lorens Mishra

Default administrator account created by the installer.

Continue reading

All Python notes →
Python

Classes and Objects

A class describes a kind of thing; an object is one of them. Python builds every object the same way, and understanding that removes most of the myste...

Read more

Discussion

0 comments
Sign in to join the discussion.

No comments yet. Be the first to say something.