Layer 2 Session 13 Object-Oriented Basics

Class Anatomy โ€” __init__ and Attributes

An empty class is not useful. This session gives every Country instance its own data, using the constructor method __init__ and instance attributes.

name"Kenya" region"Africa" population54000000 key : value pairs

Every instance gets its own independent slots of data.

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 __init__ with parameters and assign them to self attributes
  • Explain what self refers to and why every method needs it as the first parameter
  • Create multiple instances with different attribute values and confirm they do not interfere with each other
  • Read and update an instance attribute from outside the class
  • Compare attribute access (dot notation) to the dictionary bracket access from Session 05

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):\n self.name = name, what does self.name = name do?

self refers to the specific instance currently being built. Assigning to self.name stores the value ON that instance, so it persists and is readable as instance.name after construction finishes.
Question 2 of 5

Why does every method inside a class, including __init__, take self as its first parameter?

When you call c.some_method(), Python automatically passes c itself as the first argument. By convention we name that parameter self, and it is how the method accesses that specific instance's data.
Question 3 of 5

Given k = Country("Kenya") and g = Country("Ghana"), what is k.name?

Each call to Country(...) runs __init__ fresh, with its own self. k and g are separate instances, each with their own independent name attribute.
Question 4 of 5

How do you read an instance attribute from outside the class, e.g. on a variable k?

Instance attributes are read with dot notation, not bracket access โ€” this is the key syntactic difference from the dictionaries in Session 05, even though the underlying idea (a named slot holding a value) is conceptually similar.
Question 5 of 5

Can you change an instance attribute after construction, e.g. k.name = "Kenya (updated)"?

Unless a class deliberately prevents it (a topic for later), instance attributes can be reassigned freely from anywhere with access to the instance, exactly like a dictionary value.

3. The Concept โ€” __init__ and Instance Attributes

YOU WRITECountry("Kenya")PYTHON RUNS__init__(self,"Kenya")

Country("Kenya") silently becomes __init__(new_blank_instance, "Kenya") โ€” self IS that new instance.

The constructor: __init__

A class gains real data through a special method called __init__, which Python calls automatically every time you construct a new instance. Its job is to set up that instance's starting attributes.

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

k = Country("Kenya", "Africa", 54000000)
print(k.name)         # "Kenya"
print(k.region)       # "Africa"
print(k.population)   # 54000000

self โ€” a reference to the specific instance

When Python runs Country("Kenya", "Africa", 54000000), it first creates a blank instance, then calls __init__(that_instance, "Kenya", "Africa", 54000000) โ€” the instance itself is silently passed as the first argument. We name that parameter self by convention, and use it to attach data to that specific instance.

class Country:
    def __init__(self, name):
        print("self is:", self)   # the instance being built
        self.name = name           # attach data to THIS instance

k = Country("Kenya")
# self is: <__main__.Country object at 0x...>

Every instance holds its own independent data

Because __init__ runs fresh for each instance, with a different self each time, instances never share attribute values by default โ€” exactly like two separate dictionaries never share keys.

k = Country("Kenya")
g = Country("Ghana")

print(k.name)  # "Kenya"
print(g.name)  # "Ghana" โ€” completely independent of k.name

Dot notation vs Session 05's bracket notation

Recall reading a dictionary value: country["name"]. An instance attribute is read the same conceptual way, but with dot notation instead of brackets, and no risk of a KeyError โ€” accessing an attribute that truly doesn't exist raises AttributeError instead, which we'll handle the same way we handled KeyError in Session 11.

country_dict = {"name": "Kenya"}
print(country_dict["name"])   # dict โ€” bracket access

k = Country("Kenya", "Africa", 54000000)
print(k.name)                  # instance โ€” dot access

# Attributes are mutable, just like dict values
k.name = "Kenya (updated)"
print(k.name)

Country now genuinely holds data of its own. Next session we give it behavior โ€” methods that operate on that data without needing it passed in as an argument.


4. Lab

Lab objective: Give Country a real constructor with name, region, and population, create multiple independent instances, and prove attributes are mutable.

What you will build

A file called country_init.py.

Step-by-step instructions

1

Create the file and define __init__

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

Create two instances with different data

kenya = Country("Kenya", "Africa", 54000000)
peru = Country("Peru", "Americas", 33000000)

print(kenya.name, kenya.region, kenya.population)
print(peru.name, peru.region, peru.population)
3

Prove the instances are independent

Change one instance's attribute and confirm the other is untouched.

kenya.population = 55000000
print("kenya.population:", kenya.population)  # 55000000
print("peru.population:", peru.population)    # still 33000000 โ€” unaffected
4

Print self inside __init__ for both instances

Add a temporary print(self) line inside __init__ to see the two different instance addresses.

class CountryDebug:
    def __init__(self, name):
        print("Building instance:", self, "with name", name)
        self.name = name

CountryDebug("Kenya")
CountryDebug("Peru")
5

Read one attribute with dot notation and compare it to Session 05's dict syntax in a comment

print(kenya.name)
# Compare to Session 05: country_dict["name"] โ€” same idea, different syntax

5. Expected Files Changed

FileActionWhy
country_init.py Created A real Country class with __init__ and instance attributes.
docs/sessions/session-13/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_init.py docs/sessions/session-13/index.html
git commit -m "session-13: give Country real attributes via __init__"
Do not commit until you can answer out loud: "What does self actually refer to inside __init__, and why does mutating kenya.population not affect peru.population?"

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):\n self.name = name, what does self.name = name do?

self refers to the specific instance currently being built. Assigning to self.name stores the value ON that instance, so it persists and is readable as instance.name after construction finishes.
Question 2 of 5

Why does every method inside a class, including __init__, take self as its first parameter?

When you call c.some_method(), Python automatically passes c itself as the first argument. By convention we name that parameter self, and it is how the method accesses that specific instance's data.
Question 3 of 5

Given k = Country("Kenya") and g = Country("Ghana"), what is k.name?

Each call to Country(...) runs __init__ fresh, with its own self. k and g are separate instances, each with their own independent name attribute.
Question 4 of 5

How do you read an instance attribute from outside the class, e.g. on a variable k?

Instance attributes are read with dot notation, not bracket access โ€” this is the key syntactic difference from the dictionaries in Session 05, even though the underlying idea (a named slot holding a value) is conceptually similar.
Question 5 of 5

Can you change an instance attribute after construction, e.g. k.name = "Kenya (updated)"?

Unless a class deliberately prevents it (a topic for later), instance attributes can be reassigned freely from anywhere with access to the instance, exactly like a dictionary value.

9. Reflection Questions

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

  1. If you forgot to write self.name = name and just wrote name = name inside __init__, what do you predict would happen when you tried kenya.name afterward? Try it.
  2. Why does Python require self to be listed explicitly as the first parameter instead of making it implicit like some other languages do?
  3. What is the practical difference between a dictionary's keys and a class instance's attributes, now that you've used both?
  4. Can you think of a rule (like Session 11's population validation) that __init__ should probably enforce, that it does not yet?

10. What Breaks If This Knowledge Is Missing?

  • The self.x = x omission bug: Forgetting self. in an assignment inside __init__ is an extremely common beginner mistake โ€” the value is assigned to a local variable that vanishes when the method returns, and the instance ends up with no such attribute at all, causing an AttributeError later when you try to read it.
  • Methods (Session 14): Every method you write from the next session forward relies on the exact same self mechanism you just learned. If self is not solid now, method definitions will look like unexplained boilerplate.
  • Constructor validation (Layer 4 data contracts): In Session 30 we will formalize what a "valid" Country looks like using type hints and dataclasses โ€” that entire session assumes you deeply understand what __init__ is actually doing today.

11. What We Learned

Python concept mastered: __init__ as the constructor, self as the reference to the instance being built, and instance attributes as independent per-instance data.

Unlocks: Country instances can now hold real, independent data โ€” the foundation every remaining session in the project builds on.

Next session: Session 14 โ€” Instance Methods. We give Country behavior โ€” methods that operate on its own data without needing that data passed in as an argument every time.