AlgoMaster Logo

Instance Methods

High Priority16 min readUpdated June 6, 2026
Listen to this chapter
Unlock Audio

An instance method is a function defined inside a class body that operates on a particular instance. It's the most common method type by a wide margin: most methods written in Python code fall into this bucket. This lesson digs into what makes an instance method different from a plain function, how Python wires self to the instance, what a bound method is, and how to design instance methods that are clean and easy to use.

What Makes a Method "Instance"

A regular function lives on its own. It takes arguments, runs some logic, and returns a value. Nothing about the function is tied to a particular object.

An instance method takes the same shape, except it lives inside a class and its first parameter is always self. The function body uses self to read and write the data of one specific instance, the one the method was called on.

The function apply_discount is the same idea in both versions. The difference is who owns the data. In the plain-function version, the caller has to pass the price in every time and reassign the return value. In the method version, the price lives on the instance, the method reaches it through self, and the change persists on the object without the caller juggling anything. That's the entire point of an instance method: bind a piece of behavior to the data it operates on, so neither one floats around alone.

An instance method only makes sense when called on an instance; without one there's no self to bind. The method can both read and write instance attributes through self, so it can either inspect state or change it. The same method definition is shared by every instance of the class; there isn't a separate copy of apply_discount attached to every Product. The reason is covered in the next section.

The first parameter is named self by every Python library. Python doesn't enforce the name, but every linter, IDE, and reader expects it. Use self.

Where the Method Actually Lives

A method defined inside a class body is stored on the class, not on each instance. Every instance created shares that one function. Reading it through an instance is the lookup that wires self up.

Product.describe (reading the attribute through the class) returns a plain function. It isn't bound to any instance. mouse.describe (reading through an instance) returns a bound method: a wrapper that remembers both the function and the instance it was looked up on. Two bound methods for two different instances are not the same object, because each one carries its own self, even though they share the same underlying function.

A diagram makes the relationship clearer. The function lives once on the class. Each instance has its own attributes. Writing mouse.describe builds a small wrapper on the fly that remembers "this function, this instance":

The cyan node is the class. It holds the one and only describe function. The orange nodes are two instances; each has its own data. The green nodes are bound methods, built fresh each time mouse.describe or keyboard.describe is accessed. The bound method is glue: it remembers which function to call and which instance to pass as self.

This design has two practical effects. Memory stays small, because adding ten thousand Product instances doesn't add ten thousand copies of describe. And changing the class definition affects every instance at once, because they all look up methods on the same class object.

How instance.method() Dispatches

The "dot" in mouse.describe() does a small but real amount of work. Walking through it once removes most of the mystery from how methods behave.

When Python evaluates mouse.describe(), it splits into two steps:

  1. Evaluate mouse.describe (the attribute access). This is the lookup step.
  2. Call the result with the arguments inside the parentheses.

The lookup step is where bound methods come from. Python first checks whether mouse itself has an attribute called describe. It doesn't, so Python walks up to the class Product and finds the function there. Because the function was looked up through an instance, Python doesn't return the raw function. It returns a bound method built by calling function.__get__(instance, class) through the descriptor protocol. The descriptor protocol is what makes this work, but the detail isn't needed to use it.

The first line is the normal idiom. The second line spells out the underlying operation: look up describe on the class as a plain function, then call it with mouse as the first argument. The third line goes one level deeper and shows the descriptor call that builds the bound method. Line three rarely appears in real code, but seeing it once explains the second line.

The takeaway: the dot in mouse.describe is a lookup followed by a small wrapping step that pins the function to self. The parentheses then call the wrapped result. Calling methods through the class (as in line 2) is the same operation without the sugar.

Calling a Method on the Class Directly

Because the function itself lives on the class, it can be called through the class with the instance passed in by hand. This isn't typical production code, but it's the same machinery the dot uses, and it's useful in three situations: debugging, calling a method from a parent class explicitly, and writing helper code that doesn't already have a bound method.

Both calls do the same thing. mouse.apply_discount(10) is sugar for Product.apply_discount(mouse, 10). The dotted form is the common choice because it's shorter and clearer. The class-level form is what shows up in tracebacks ("Product.apply_discount takes 2 positional arguments but 3 were given"), in code that walks the class hierarchy, and inside super() calls when a subclass needs the parent's version of a method.

One detail to keep in mind: calling Product.apply_discount(mouse, 10) with a mouse of a different (unrelated) class isn't rejected at the call site. Python runs the function with whatever is passed. The body might still work if the foreign object happens to have the attributes the method reads; it might fail later with an AttributeError. The safety of self being "the right type" comes from how methods are called, not from any check Python performs.

Reading vs Mutating Through self

Inside the method body, self is a parameter that points to the instance. Attributes can be read with self.something and written with self.something = value. There's no special syntax or distinction between "read methods" and "write methods"; it's all normal attribute access through a parameter.

The add method writes to self.items and self.subtotal; the show method only reads. Both go through the same self. There's no decorator or naming convention that marks one as a writer and the other as a reader; Python doesn't care, and the difference is purely about what the body chooses to do.

A couple of patterns are worth knowing. Methods that mutate the instance typically return None, because the caller already has a reference to the changed object and doesn't need it back. Methods that compute and return a value typically don't mutate anything; they read off self and return the result. This split isn't enforced, but it matches how most Python code is written, and it makes call sites easier to reason about.

cart.add(...) returns None, which is normal for mutating methods. cart.count() returns the value the caller asked for. Mixing both behaviors in one method (mutating and returning a useful value) is sometimes the right call, but pause to ask "does this method really need to do two things at once?" before doing it.

Mutating list methods like list.append, list.sort, and dict.update return None. Writing self.items = self.items.append(item) looks reasonable but sets self.items to None. Call mutating methods on their own line.

Method Lookup From Instance vs Class

Python's attribute lookup follows a fixed order. With obj.attr, the instance's own dictionary is checked first, then the class, then the class's parents. For methods, this matters because a method on a single instance can be shadowed by accidentally assigning to its name.

The second mouse.describe is a plain attribute that the instance now owns directly, set with normal assignment. When Python looks up describe on mouse, it finds the instance attribute first and returns the lambda, never reaching the class. The lambda isn't a bound method, so it doesn't receive self; that's why the lambda has no parameters.

This is almost never something to do on purpose. The more useful takeaway is the other direction: if a method "doesn't work as expected", check whether something has been written to that name on the instance. A stray obj.method = some_value line is a real debugging story for those unfamiliar with the lookup rules.

A second variant of the same idea: assigning a regular function to an instance attribute does not turn it into a bound method. Only functions defined inside a class body, looked up through an instance, become bound methods.

mouse.shout is a plain function attribute. There's no self passed in. For bound-method behavior, define the function inside the class body, where Python's descriptor protocol activates. Attaching a function to an instance gives a callable, but nothing more.

When an Instance Method Fits

Not every function that touches a class should be an instance method. The test is simple: does the function need access to a specific instance's state? If yes, instance method. If it only needs class-level state (or no state at all), use a class method or static method instead.

Cases where an instance method is the right choice:

  • The function reads instance attributes to do its job (cart.subtotal(), product.describe()).
  • The function modifies instance attributes (cart.add(item), order.mark_shipped()).
  • The function returns something derived from the instance's state (review.is_positive(), customer.full_name()).

Cases where an instance method is overkill:

  • The function takes only its parameters and ignores self entirely. That's a hint it should be a @staticmethod or, more often, a module-level function.
  • The function needs class-level data (a registry, a default config) but never per-instance data. That's the @classmethod case.

The decision usually shows up while writing the method body. If self. appears on every line, the method belongs on the instance. If self never appears in the body, the method probably doesn't belong on the instance.

Designing Good Instance Methods

A clean instance method does one thing, names that thing clearly, and either mutates or returns, not both. The same rules that apply to plain functions apply to methods, with a couple of extras.

Keep them small. A method longer than 20 to 30 lines is usually doing too much. Break it apart. Instance methods are easy to extract because they can call each other through self.

Name them as verbs (or verb phrases) for actions, and as nouns or adjectives for queries. cart.add(item) is a verb because it does something. cart.is_empty() returns a boolean; the is_ prefix signals the shape of the return value. cart.total() returns a value derived from state. The naming gives the reader a hint about what kind of work the method does without requiring a read of the body.

Don't mix mutation and a non-trivial return value. A method that adds an item to a cart and also returns the cart's new total is doing two unrelated jobs. Split it. The exception is methods that return self to support chaining (builder.add(x).add(y).build()), and even that pattern is divisive in Python.

Validate inputs at the method boundary. If a method has preconditions (a discount can't be over 100%, a quantity can't be negative), check them at the top of the method and raise a clear exception. Don't trust the caller to have already validated.

The check at the top of apply_discount keeps bad inputs from corrupting self.price. Without it, a typo like apply_discount(150) would set the price to a negative number without warning, and the bug would show up much later, far from its cause.

Prefer immutability when it fits. If a method computes a new value from the instance's state, return the new value instead of mutating in place. The mutation version is sometimes the right call (orders really do change status over time), but for derived data, returning a new value is usually cleaner.

discounted_price answers a question without changing anything. discount would have changed self.price permanently. Both are valid; the right choice depends on whether the caller wants to update the product or just see what the price would be.

A Small Worked Example: An Order Object

Pulling the ideas together, an Order class with several instance methods of different shapes: some that mutate, some that read, and one that validates input.

add_item validates and mutates; it returns nothing. total and is_empty read state and return values; they don't mutate. mark_shipped mutates but also enforces a precondition (a delivered order can't be shipped). summary calls another method (self.total()) on the same instance, which is normal; methods can call each other through self.

The call site reads as order.add_item(...), order.mark_shipped(), order.summary(). Each method is named for what it does and operates on the order itself. The caller doesn't have to track which list contains what or pass the order around. That's the payoff for keeping behavior next to data.

Quiz

Instance Methods Quiz

10 quizzes