Day 11 · ~18m

Python Instance Methods: Adding Behavior to Your Objects

Methods are functions that belong to objects. The self parameter is explicit—that's intentional. Learn why and build your first Order class with discount logic.

student (thinking)

Yesterday I built my first real class — an Order that stores id, customer, and total. It validated negative quantities in __init__. That part made sense. But I want to add a discount method to it, and I'm stuck on how methods actually work.

teacher (neutral)

What did you try?

student (focused)

I wrote this:

teacher (neutral)
def apply_discount(order, pct):
    order['total'] *= (1 - pct / 100)
    return order['total']

It's a function that takes an order and a percent. Works fine as a standalone function.

teacher (serious)

And then you realize the order is not a dict anymore — it's your Order object from yesterday.

student (confused)

Right. So I can't use dict syntax. I would need order.total. But then what? Do I add that function to the class somehow?

teacher (encouraging)

You put it inside the class definition. That is what makes it a method instead of a function.

class Order:
    def __init__(self, id, customer, total):
        self.id = id
        self.customer = customer
        self.total = total

    def apply_discount(self, pct):
        self.total *= (1 - pct / 100)
        return self.total

order = Order(101, "Alice Chen", 100.0)
print(order.apply_discount(10))  # 90.0
student (thinking)

Okay so the function lives inside the class. But why is there a self parameter? Why not just:

teacher (neutral)
def apply_discount(pct):
    self.total *= (1 - pct / 100)
    return self.total
teacher (serious)

Because there is no magic variable called self in Python. When you write a method, you are still writing a function. That function has to know which order it is operating on.

student (confused)

But when I call order.apply_discount(10), doesn't Python know I mean that order?

teacher (focused)

Yes. And here is what Python actually does: when you call order.apply_discount(10), Python translates that into a function call. The function is apply_discount. The order is passed as the first argument.

Think of it like this:

# What you write:
order.apply_discount(10)

# What Python does internally:
Order.apply_discount(order, 10)
student (surprised)

Python inserts the order as the first argument automatically?

teacher (neutral)

That is the whole mechanism. Dot notation is syntax sugar. order.apply_discount(10) is a shorthand for "call the apply_discount function from the Order class, passing order as the first argument." And the first parameter in the function has to have a name. By convention, that name is self.

student (thinking)

So self is not a keyword. It is just the name of the first parameter.

teacher (encouraging)

Exactly. You could name it this or me or bananas. The language does not care. But everyone uses self. If you used anything else, your teammates would look at your code and get confused.

student (amused)

So self is just a cultural agreement.

teacher (amused)

The most important kind of convention in programming. The language is not enforcing it. The team is.

student (curious)

Okay so when I write self.total, I'm accessing the attribute of the object that was passed as the first argument.

teacher (focused)

Exactly. And when I said self.total *= (1 - pct / 100), I am modifying the actual order object, not a copy. Methods can change the object's state.

student (thinking)

Let me make sure I have this. A method is a function defined inside a class. The first parameter is always self — the instance the method was called on. When I call order.apply_discount(10), Python passes the order as self.

teacher (proud)

That is the exact definition. And now you can add more methods to Order.

student (focused)

Like, I could add a method that checks if the order is paid?

teacher (neutral)

Go ahead. Write it.

student (thinking)

Let me think... it would check some status field.

teacher (neutral)
def is_paid(self):
    return self.status == "paid"

Right? An instance method with just self as the parameter.

teacher (surprised)

Correct. And a method that returns information about the order?

teacher (neutral)
def get_summary(self):
    status = "paid" if self.status == "paid" else "pending"
    return f"{self.customer}: ${self.total:.2f} ({status})"
teacher (excited)

And now your Order class actually does things. It does not just hold data — it knows how to discount itself, how to report on its status, how to describe itself.

student (curious)

So I keep adding methods for anything the order should be able to do?

teacher (encouraging)

That is the idea behind objects. The object owns its data. The object owns the behavior on that data. Someone asks "is this order paid?", the order knows the answer. Someone asks "apply a discount", the order applies it and updates itself. The order is not just a dumb data container anymore.

student (thinking)

This is starting to feel like what Amir writes. Objects that know how to operate on themselves.

teacher (focused)

Exactly. And the reason self is explicit — why you have to write it as a parameter — is so you can always see it. When you read self.total, you know exactly what you are operating on. Some languages hide the self and call it implicit. Python says: "No. The object you are working with is always visible." It is verbose by design.

student (amused)

I know self feels redundant. But the reason it exists is that someone decided readability matters more than brevity.

teacher (serious)

I know self feels redundant. Here is the actual reason it exists. You can always see what object you are working with. It is not hidden behind magic. When someone reads your method, they see self.total and they know: "this is accessing the instance variable total on whatever order this method was called on." That clarity is worth the extra word.

student (focused)

Alright. So my Order class now has __init__, apply_discount, is_paid, and get_summary methods. When I call order.get_summary(), it returns a string that describes the order.

teacher (neutral)

Try it. Build the Order class with all four methods in the challenge.

student (curious)

One thing — when I call order.get_summary(), does that return the same thing every time?

teacher (focused)

Every time you call it, yes — for that order at that moment. If you modify the order with apply_discount, the next call to get_summary() will reflect the new total.

order = Order(101, "Alice Chen", 100.0, "pending")
print(order.get_summary())  # Alice Chen: $100.00 (pending)
order.apply_discount(10)
print(order.get_summary())  # Alice Chen: $90.00 (pending) — total changed
student (thinking)

So the method is reading live data from the object.

teacher (encouraging)

Always live. Methods do not cache or store — they look at the current state.

student (curious)

What if I have multiple Order instances? Do they share methods or does each one have its own copy?

teacher (neutral)

They share the method code. But each instance has its own state. Watch:

order1 = Order(101, "Alice Chen", 100.0, "paid")
order2 = Order(102, "Bob Kumar", 50.0, "pending")

order1.apply_discount(10)  # only order1 changes
print(order1.get_summary())  # Alice Chen: $90.00 (paid)
print(order2.get_summary())  # Bob Kumar: $50.00 (pending) — unchanged
student (excited)

Oh. The method is in the class, but the data is in each instance.

teacher (proud)

That is the whole separation. The class defines the methods — the behavior. Each instance has its own attributes — the data. Ten thousand orders use the same apply_discount method, but each one has its own total.

student (focused)

Okay so when I call order.apply_discount(pct), Python passes the specific order as self. The method reads and writes self.total for that specific order.

teacher (serious)

That is exactly right. And because self is explicit in the signature, you always know what you are modifying. There is no mystery. No hidden receiver. No implicit context. You see self.total and you know: this modifies the instance's total, not some global total or a copy.

student (thinking)

I think I finally get why Amir's Order class looks the way it does. Each order knows what it is, and each order knows what it can do.

teacher (encouraging)

That is object-oriented programming. Not inheritance and polymorphism and all the big words. Just: data and the methods that operate on that data, living together in one place.

student (curious)

Alright, one more thing. When I print an order — like print(order) — it just shows <Order object at 0x...>. Can I fix that?

teacher (neutral)

Tomorrow. There is a special method called __repr__ that tells Python how to display an object when you print it. For now, use order.get_summary() to see the order.

student (focused)

So there are more special methods like __init__ and __repr__?

teacher (serious)

About thirty of them. __init__ is the constructor. __repr__ is "how do I show this as a string." __eq__ is "how do I compare two of these." __lt__ is "how do I check if this is less than that." But for now, focus on writing regular methods. The special methods can wait.

student (excited)

I want to build this Order class and add some methods of my own.

teacher (encouraging)

That is the challenge. Define apply_discount as a method, not a standalone function. See what it feels like to have the order modify itself.