Python The self Keyword

In Python, self is the name conventionally given to the first parameter of every instance method — it is how an object refers to itself from inside its own methods. When you call a method on an object, Python automatically passes that object in as the first argument, and by convention we name that parameter self. Without it, a method would have no way to read or change the attributes that belong to the specific object it was called on, or to call that object’s other methods. Understanding self deeply is really understanding how Python’s entire object model works.

Overview: What self Actually Is

A class is a blueprint. When you instantiate it, Python creates an object in memory and that object needs a way to store its own data (its attributes) separately from every other object created from the same class. Methods defined inside a class are just ordinary functions — but when you access one through an instance, Python does something special: it turns the function into a bound method, automatically supplying the instance as the first argument. That first argument is what we name self.

Here is the important part: self is not a reserved keyword in Python, unlike this in Java, C++, or JavaScript. You could technically name that first parameter anything — this, me, obj — and the code would still run. But every piece of Python code you will ever read, and every style guide (including PEP 8), uses self. Deviating from this convention makes your code confusing to every other Python developer, so treat it as a hard rule even though the interpreter does not enforce it.

When Python looks up instance.method(args), it actually performs two steps behind the scenes: it finds method on the class (not the instance), and because functions are descriptors in Python, accessing a function through an instance returns a bound method object that already has the instance baked in as the first argument. That bound method is then called with the remaining arguments. In other words, instance.method(args) is exactly equivalent to Class.method(instance, args). This is why every instance method’s signature must include self as its first parameter — Python is going to supply it automatically, and the method definition must have a place to receive it.

Inside a method, self.attribute_name reads or writes a value stored in the instance’s own namespace (technically its __dict__). If the attribute isn’t found there, Python falls back to looking it up on the class (which is how class attributes — values shared by every instance — work) and then up the method resolution order for inherited classes. This is also why two different objects created from the same class can hold completely different data: each has its own self, and therefore its own private storage.

Syntax

The general shape of an instance method always starts with self as the first formal parameter:

class ClassName:
    def method_name(self, other_param1, other_param2=default):
        # use self.attribute to read/write instance data
        # use self.other_method() to call another instance method
        ...
  • self — the first parameter of every instance method; refers to the specific object the method was called on. You never pass it explicitly when calling instance.method() — Python supplies it.
  • self.attribute_name — reads or assigns an attribute that belongs to this particular instance.
  • self.other_method() — calls another method defined on the same class, on the same instance.
  • other parameters — any additional arguments the method needs, supplied normally by the caller after self.

Two related conventions worth knowing now: inside a @classmethod, the first parameter is named cls (it receives the class itself, not an instance), and a @staticmethod receives neither self nor cls because it doesn’t operate on instance or class state at all.

Examples

Example 1: Storing and using per-instance data

class Dog:
    def __init__(self, name, breed):
        self.name = name
        self.breed = breed

    def bark(self):
        print(f"{self.name} says Woof!")

buddy = Dog("Buddy", "Golden Retriever")
rex = Dog("Rex", "German Shepherd")

buddy.bark()
rex.bark()
print(buddy.name, buddy.breed)

Output:

Buddy says Woof!
Rex says Woof!
Buddy Golden Retriever

Both buddy and rex are built from the same Dog class, but each has its own self.name and self.breed. When buddy.bark() runs, self inside that call is buddy; when rex.bark() runs, self is rex. That is the entire mechanism that makes object-oriented state possible in Python.

Example 2: self reveals how method calls really work

class Dog:
    def __init__(self, name):
        self.name = name

    def rename(self, name):
        print(f"Renaming {self.name} to {name}")
        self.name = name

d = Dog("Buddy")
d.rename("Max")
print(d.name)

# Calling through the class explicitly, passing the instance manually
Dog.rename(d, "Charlie")
print(d.name)

Output:

Renaming Buddy to Max
Max
Renaming Max to Charlie
Charlie

This example proves the equivalence directly: d.rename("Max") and Dog.rename(d, "Charlie") do exactly the same kind of thing. In the first call, Python auto-binds d as self. In the second call, we bypass that convenience and pass the instance manually as the first positional argument to the unbound function stored on the class. Both work identically — this is proof that self is just a regular parameter receiving a regular argument, nothing magical.

Example 3: A realistic class with several self-using methods

class BankAccount:
    interest_rate = 0.02  # class attribute shared by all accounts

    def __init__(self, owner, balance=0):
        self.owner = owner
        self.balance = balance

    def deposit(self, amount):
        self.balance += amount
        return self

    def withdraw(self, amount):
        if amount > self.balance:
            print(f"Insufficient funds for {self.owner}")
        else:
            self.balance -= amount
        return self

    def apply_interest(self):
        self.balance += self.balance * self.interest_rate
        return self

    def __repr__(self):
        return f"BankAccount(owner={self.owner!r}, balance={self.balance:.2f})"

acc = BankAccount("Alice", 100)
acc.deposit(50).withdraw(30).apply_interest()
print(acc)

Output:

BankAccount(owner='Alice', balance=122.40)

Here self is used in three distinct ways: self.balance and self.owner store per-account data; self.interest_rate reads a class attribute (shared by every account, found via fallback lookup since it isn’t in the instance’s own __dict__); and each method return self, which lets calls be chained together (deposit(...).withdraw(...).apply_interest()) because each call returns the very same object it was invoked on.

How self Works Under the Hood

When you write class Dog: and define def bark(self): ... inside it, Python stores bark as a plain function object as an attribute of the class Dog — nothing instance-specific has happened yet. The binding to a particular object only happens at attribute access time:

  1. You write buddy.bark. Python looks for bark first on the instance buddy (not found), then on its class Dog (found — it’s a function).
  2. Because functions implement the descriptor protocol (__get__), accessing a function through an instance doesn’t return the raw function — it returns a new, temporary bound method object that remembers both the function and the instance (buddy).
  3. You call that bound method with (). The bound method inserts buddy as the first argument automatically and calls the original function, which receives it as self.

This is also why calling a method on the class itself, like Dog.bark(buddy), works without any binding magic: Dog.bark retrieves the plain, unbound function, so you must supply the instance yourself as the first argument — exactly as Example 2 demonstrated.

Common Mistakes

Mistake 1: Forgetting self in the method definition

If you omit self from a method’s parameter list but still call it the normal way, Python still auto-supplies the instance as the first argument — it just has nowhere to put it, so the extra argument collides with your other parameters (or, with zero declared parameters, you get a TypeError about too many arguments):

class Dog:
    def bark():  # missing self
        print("Woof!")

d = Dog()
d.bark()  # TypeError: bark() takes 0 positional arguments but 1 was given

The fix is simple: every instance method needs self as its first parameter, full stop, even if the method body never uses it.

Mistake 2: Forgetting the self. prefix when assigning an attribute

A very common bug is assigning to a local variable that merely looks like an attribute, instead of assigning to self.attribute. The code runs without any error, which makes it especially dangerous — the class silently fails to update its own state:

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

    def increment(self):
        count = self.count + 1  # forgot self on the left side
        print(count)

c = Counter(5)
c.increment()
c.increment()
print(c.count)

Output:

6
6
5

Notice that increment() prints 6 both times, and c.count is still 5 at the end — the local variable count was recalculated and printed each time, but it never actually updated the object’s real attribute, because the code never wrote to self.count. The correct version is self.count = self.count + 1 (or self.count += 1).

Best Practices

  • Always name the first parameter of an instance method self — it is a universal convention, and any other name will confuse readers even though Python allows it.
  • Use self.attribute consistently for anything that should be per-instance state; never rely on a bare local variable with the same name as an attribute.
  • Reserve class-level attributes (defined directly in the class body, without self) for values genuinely shared across all instances, such as constants or defaults — and remember that self.attribute = value inside a method creates or shadows an instance attribute rather than modifying the shared class attribute.
  • Use @classmethod with cls instead of self when a method needs to work with the class itself (e.g. alternative constructors), and @staticmethod with neither when a method doesn’t need instance or class data at all.
  • Return self from a method only when you deliberately want to support method chaining (a fluent interface) — don’t do it as a habit for every method, since it can make code less predictable.
  • Never call instance methods via the unbound Class.method(instance, ...) form in production code; it’s useful for teaching how binding works, but instance.method(...) is the idiomatic, readable form.

Practice Exercises

  1. Write a Rectangle class whose __init__ takes width and height and stores them via self. Add an area(self) method and a perimeter(self) method that use self.width and self.height to compute and return their results.
  2. Create a Playlist class with an instance attribute self.songs (a list, initialized empty in __init__). Add an add_song(self, title) method that appends to self.songs and returns self, so that two calls can be chained: playlist.add_song("A").add_song("B").
  3. Take the buggy Counter class from the Common Mistakes section and fix the increment method so that calling it twice on a Counter(5) instance correctly prints 6 then 7, and c.count equals 7 afterward.

Summary

  • self is the conventional name for the first parameter of every instance method — it refers to the specific object the method was called on.
  • self is not a reserved keyword; Python enforces the position, not the name, but every Python developer uses self by convention.
  • instance.method(args) is automatically translated by Python into Class.method(instance, args) via the descriptor protocol, which is why self must be declared explicitly in every method signature.
  • self.attribute reads or writes data that belongs to one specific instance, stored in that instance’s own namespace.
  • Forgetting self in a method definition causes a TypeError; forgetting the self. prefix when assigning silently creates a disconnected local variable instead of updating the object.
  • cls replaces self in @classmethod, and neither appears in @staticmethod.