Layer 2 Session 14 Object-Oriented Basics

Instance Methods

A class with only attributes is still just a fancier dictionary. Methods are functions that live on the class and operate on an instance's own data through self.

x f(x) result a function is just a labeled transformation

Behavior that lives with the data it operates on.

Estimated time: 35โ€“40 minutes  ยท  Pre-quiz โ†’ Concept โ†’ Lab โ†’ Commit โ†’ Post-quiz

1. Learning Objective

By the end of this session you will be able to:

  • Define an instance method that reads self attributes
  • Call a method on an instance using dot notation
  • Write a method that both reads and updates self attributes
  • Explain why a method does not need its data passed in as an argument, unlike the standalone functions from Layer 1
  • Compare a method call to the equivalent standalone-function call from Session 08

2. Pre-Coding Quiz

Answer these before reading the concept explanation or writing any code. It is fine to get these wrong โ€” that is the point. You need 4/5 to proceed to the lab.

Question 1 of 5

Given class Country:\n def __init__(self, name, population):\n self.name = name\n self.population = population\n def summary(self):\n return f"{self.name}: {self.population}", what does k.summary() return for k = Country("Kenya", 54000000)?

Calling k.summary() runs the method with self bound to k. Inside, self.name is "Kenya" and self.population is 54000000, producing "Kenya: 54000000".
Question 2 of 5

Why does calling k.summary() not require you to pass k as an argument yourself, e.g. summary(k)?

The dot-call syntax k.summary() is exactly the mechanism from Session 13 in reverse: Python automatically supplies k as the first argument (self) to summary. You only supply the remaining arguments explicitly.
Question 3 of 5

Given a method def grow_population(self, amount):\n self.population += amount, what does k.grow_population(1000000) do?

Just like a dictionary or list, mutating self.population inside a method changes the actual instance k โ€” the same instance you called the method on, in place. Nothing needs to be reassigned.
Question 4 of 5

Compare method calls to the Layer 1 approach: In Session 08 we wrote get_population(country_dict). What is the equivalent as a method?

This is the whole point of the shift: instead of passing the data as an explicit argument to a standalone function, the data (self) is now implicit, because the method lives on the object that owns the data.
Question 5 of 5

If Country defines a method called region_label(self), can you call it as Country.region_label(k) instead of k.region_label()?

The dot-call syntax is genuinely just convenient shorthand. k.region_label() and Country.region_label(k) do exactly the same thing โ€” the first form is simply what everyone writes in practice.

3. The Concept โ€” Instance Methods

YOU WRITEk.summary()PYTHON RUNSCountry.summary(k)

The dot-call is shorthand: k.summary() and Country.summary(k) are the same call.

A method is a function defined inside a class

Like __init__, every method takes self as its first parameter, giving it access to that instance's own attributes โ€” no need to pass the data in as an argument, because the method already lives on the object that owns it.

class Country:
    def __init__(self, name, region, population):
        self.name = name
        self.region = region
        self.population = population

    def summary(self):
        return f"{self.name} ({self.region}): pop. {self.population:,}"

k = Country("Kenya", "Africa", 54000000)
print(k.summary())  # "Kenya (Africa): pop. 54,000,000"

What the dot-call actually does

When you write k.summary(), Python translates this into Country.summary(k) behind the scenes โ€” k is automatically supplied as self. This is the exact same mechanism from Session 13's __init__, just used for a different method.

print(k.summary())            # normal way
print(Country.summary(k))     # exactly equivalent โ€” proves it

A method that mutates the instance

Methods can update self's attributes just like the standalone functions from Layer 1 updated dict keys โ€” except now the mutation logic lives with the data it protects.

class Country:
    def __init__(self, name, population):
        self.name = name
        self.population = population

    def grow_population(self, amount):
        if amount < 0:
            raise ValueError("amount must be non-negative")
        self.population += amount

k = Country("Kenya", 54000000)
k.grow_population(1000000)
print(k.population)  # 55000000 โ€” mutated in place, no reassignment needed

Standalone function (Layer 1) vs method (Layer 2) โ€” side by side

This is the entire conceptual leap of Layer 2: the data moved inside the object, so the function moved with it.

# Layer 1 style โ€” Session 08
def get_population(country_dict):
    return country_dict["population"]

print(get_population({"population": 54000000}))

# Layer 2 style โ€” this session
class Country:
    def __init__(self, population):
        self.population = population
    def get_population(self):
        return self.population

k = Country(54000000)
print(k.get_population())  # no argument needed โ€” self already has it

4. Lab

Lab objective: Add a summary method and a mutating grow_population method to Country, then prove the dot-call is equivalent to calling through the class directly.

What you will build

A file called country_methods.py.

Step-by-step instructions

1

Create the file with __init__ and a summary method

# country_methods.py
class Country:
    def __init__(self, name, region, population):
        self.name = name
        self.region = region
        self.population = population

    def summary(self):
        return f"{self.name} ({self.region}): pop. {self.population:,}"
2

Create an instance and call summary()

kenya = Country("Kenya", "Africa", 54000000)
print(kenya.summary())
3

Prove k.summary() and Country.summary(k) are the same call

print(kenya.summary() == Country.summary(kenya))  # True
4

Add a mutating grow_population method with validation

Reuse the ValueError pattern from Session 11.

class Country:
    def __init__(self, name, region, population):
        self.name = name
        self.region = region
        self.population = population

    def summary(self):
        return f"{self.name} ({self.region}): pop. {self.population:,}"

    def grow_population(self, amount):
        if amount < 0:
            raise ValueError(f"amount must be non-negative, got {amount}")
        self.population += amount

kenya = Country("Kenya", "Africa", 54000000)
kenya.grow_population(1000000)
print(kenya.summary())
5

Call grow_population with a negative amount inside try/except

Reuse the try/except pattern from Session 11 โ€” confirm the exception is raised and caught.

try:
    kenya.grow_population(-500)
except ValueError as e:
    print("Rejected:", e)

5. Expected Files Changed

FileActionWhy
country_methods.py Created Adds summary() and grow_population() instance methods to Country.
docs/sessions/session-14/index.html Created This session document.
If you find yourself editing any other file, stop. This session touches exactly 2 files.

6. Commit Checkpoint

Once the lab is complete and you can explain every line, make this exact commit:

git add country_methods.py docs/sessions/session-14/index.html
git commit -m "session-14: add summary and grow_population instance methods"
Do not commit until you can answer out loud: "Why does grow_population not need the country passed in as an argument the way Session 08's functions did?"

7. Code Review Checklist

Go through your code line by line and check each item:


8. Post-Coding Quiz

Same 5 questions. Take it again now that you have written and run the code. You need 4/5 to mark this session complete.

Question 1 of 5

Given class Country:\n def __init__(self, name, population):\n self.name = name\n self.population = population\n def summary(self):\n return f"{self.name}: {self.population}", what does k.summary() return for k = Country("Kenya", 54000000)?

Calling k.summary() runs the method with self bound to k. Inside, self.name is "Kenya" and self.population is 54000000, producing "Kenya: 54000000".
Question 2 of 5

Why does calling k.summary() not require you to pass k as an argument yourself, e.g. summary(k)?

The dot-call syntax k.summary() is exactly the mechanism from Session 13 in reverse: Python automatically supplies k as the first argument (self) to summary. You only supply the remaining arguments explicitly.
Question 3 of 5

Given a method def grow_population(self, amount):\n self.population += amount, what does k.grow_population(1000000) do?

Just like a dictionary or list, mutating self.population inside a method changes the actual instance k โ€” the same instance you called the method on, in place. Nothing needs to be reassigned.
Question 4 of 5

Compare method calls to the Layer 1 approach: In Session 08 we wrote get_population(country_dict). What is the equivalent as a method?

This is the whole point of the shift: instead of passing the data as an explicit argument to a standalone function, the data (self) is now implicit, because the method lives on the object that owns the data.
Question 5 of 5

If Country defines a method called region_label(self), can you call it as Country.region_label(k) instead of k.region_label()?

The dot-call syntax is genuinely just convenient shorthand. k.region_label() and Country.region_label(k) do exactly the same thing โ€” the first form is simply what everyone writes in practice.

9. Reflection Questions

Think through these after the post-quiz. No right answer โ€” they are for discussion.

  1. Rewrite grow_population as a Layer-1-style standalone function taking a dict. How many more characters does the caller have to type versus the method version?
  2. Why does putting validation logic (the ValueError check) inside the method make it harder to accidentally bypass than a separate validation function would?
  3. What would happen if you defined grow_population without self as the first parameter? Try it and read the error message carefully.
  4. Can every standalone function from Layer 1 become a method? Can you think of one that could not?

10. What Breaks If This Knowledge Is Missing?

  • Missing self bugs: Forgetting self as a method's first parameter produces a confusing TypeError about argument counts when the method is called normally โ€” a very common early mistake that this session should immunize you against.
  • Passing data (Session 15): Right now every instance is built by passing all fields directly to __init__. In the next session, we formalize this as "props" flowing into an object at construction time, drawing an explicit line back to the original React course's prop pattern.
  • Testing methods (Layer 5): Every unit test you write from Session 33 onward calls methods on instances exactly the way this lab does. If method calls are not second nature, reading test code will be much harder.

11. What We Learned

Python concept mastered: Instance methods โ€” self-bound functions that read and mutate an instance's own data without needing it passed in explicitly.

Unlocks: Country is now a genuine object: data plus the behavior that belongs with it. This is the last piece needed before we start composing objects together.

Next session: Session 15 โ€” Passing Data via the Constructor. We formalize how data flows into an object at construction time, and start passing whole objects (not just primitive values) into other objects.