Layer 2 Session 18 Object-Oriented Basics

Building Lists of Objects

Real data rarely arrives as neatly hand-typed Country(...) calls. This session formalizes converting a list of raw dicts into a list of proper Country instances, at any scale.

class Country Kenya Peru Ghana

Many raw records, converted into many working objects.

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:

  • Convert a list of raw dictionaries into a list of class instances using a comprehension
  • Write a classmethod that constructs an instance from a dictionary
  • Explain what @classmethod does and how cls differs from self
  • Handle a malformed record in the raw data without crashing the whole batch
  • Combine everything from Layer 1 (comprehensions) and Layer 2 (classes) into one operation

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 raw = [{"name": "Kenya", "region": "Africa", "population": 54000000}, ...], which builds a list of Country instances from it?

Recall Session 09: **r spreads each dictionary's key-value pairs as keyword arguments into the Country constructor โ€” exactly matching __init__'s parameter names.
Question 2 of 5

What does @classmethod do to a method like from_dict?

A classmethod is bound to the class, not a specific instance. Its first parameter, cls, refers to the class itself โ€” useful for alternate constructors, since you don't have an instance yet at the point you're trying to build one.
Question 3 of 5

Given @classmethod\ndef from_dict(cls, data):\n return cls(**data), what does Country.from_dict({"name": "Kenya", "region": "Africa", "population": 54000000}) return?

Inside a classmethod, cls IS the class (Country), so cls(**data) is equivalent to calling Country(**data) โ€” constructing a new instance from the dictionary's keys and values.
Question 4 of 5

Why prefer a from_dict classmethod over just calling Country(**d) directly everywhere you need to convert a dict?

If the raw data's shape changes later (a renamed key, a new required default), you only need to update from_dict once, instead of hunting down every place Country(**d) was called directly.
Question 5 of 5

One raw record is missing the "population" key entirely. Using [Country.from_dict(r) for r in raw] with no error handling, what happens when that record is processed, assuming population has no default in __init__?

Without a default value or error handling, a missing required keyword argument raises a TypeError, which โ€” per Session 11 โ€” will propagate and stop the whole batch unless you wrap the conversion in a try/except to handle bad records individually.

3. The Concept โ€” Building Object Lists from Raw Data

RAW DICTS{...}, {...}COUNTRY(**R)per itemINSTANCESCountry, Country

Each raw dict is spread with ** into a fresh Country() call, one per item โ€” the comprehension pattern from Session 07, now building instances instead of values.

From raw dicts to instances, using what you already know

This session combines Session 07's comprehensions with Session 09's ** spreading and Session 13's constructors โ€” nothing new syntactically, just a new combination.

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:,}"

raw = [
    {"name": "Kenya", "region": "Africa", "population": 54000000},
    {"name": "Peru", "region": "Americas", "population": 33000000},
]

countries = [Country(**r) for r in raw]
print(countries[0].summary())  # "Kenya (Africa): pop. 54,000,000"

A classmethod as an alternate constructor

Repeating Country(**r) everywhere you convert a dict works, but centralizing that logic in one place on the class itself is more maintainable. A classmethod is a method bound to the class rather than an instance โ€” useful precisely because you don't have an instance yet.

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

    @classmethod
    def from_dict(cls, data):
        return cls(**data)  # cls IS the Country class here

kenya = Country.from_dict({"name": "Kenya", "region": "Africa", "population": 54000000})
print(kenya.name)  # "Kenya"

cls vs self

self (regular methods) refers to a specific instance. cls (classmethods) refers to the class itself โ€” the blueprint, not a built thing. This mirrors the class-vs-instance distinction from Session 12.

class Country:
    @classmethod
    def from_dict(cls, data):
        print("cls is:", cls)          # <class '__main__.Country'> โ€” the class itself
        return cls(**data)

    def summary(self):
        print("self is:", self)        # a specific Country instance

Handling a bad record without losing the whole batch

Session 11's try/except lets us skip malformed records individually instead of letting one bad record crash the entire conversion.

raw = [
    {"name": "Kenya", "region": "Africa", "population": 54000000},
    {"name": "Ghost Nation"},  # missing region and population!
]

countries = []
for r in raw:
    try:
        countries.append(Country.from_dict(r))
    except TypeError as e:
        print(f"Skipping malformed record {r!r}: {e}")

print(len(countries))  # 1 โ€” only the valid record made it in

4. Lab

Lab objective: Build a from_dict classmethod on Country, convert a batch of raw records with a comprehension, and gracefully skip a malformed one.

What you will build

A file called build_list_lab.py.

Step-by-step instructions

1

Create the file with Country and a from_dict classmethod

# build_list_lab.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:,}"

    @classmethod
    def from_dict(cls, data):
        return cls(**data)
2

Build 4 valid raw records and convert them with a comprehension

raw = [
    {"name": "Kenya", "region": "Africa", "population": 54000000},
    {"name": "Ghana", "region": "Africa", "population": 31000000},
    {"name": "Peru", "region": "Americas", "population": 33000000},
    {"name": "Japan", "region": "Asia", "population": 125000000},
]

countries = [Country.from_dict(r) for r in raw]
for c in countries:
    print(c.summary())
3

Add one malformed record (missing a required key)

raw_with_bad_record = raw + [{"name": "Ghost Nation"}]  # missing region, population
4

Convert with a for-loop and try/except, skipping the bad one

good_countries = []
for r in raw_with_bad_record:
    try:
        good_countries.append(Country.from_dict(r))
    except TypeError as e:
        print(f"Skipping malformed record {r!r}: {e}")

print("Converted successfully:", len(good_countries))
print("Total attempted:", len(raw_with_bad_record))
5

Print cls inside from_dict temporarily to see it is the class, not an instance

class CountryDebug(Country):
    @classmethod
    def from_dict(cls, data):
        print("cls is:", cls)
        return cls(**data)

CountryDebug.from_dict({"name": "Kenya", "region": "Africa", "population": 54000000})

5. Expected Files Changed

FileActionWhy
build_list_lab.py Created Converts raw dicts into Country instances at scale using a classmethod and comprehension.
docs/sessions/session-18/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 build_list_lab.py docs/sessions/session-18/index.html
git commit -m "session-18: build Country instances in bulk with a from_dict classmethod"
Do not commit until you can answer out loud: "Why does from_dict use cls(**data) instead of Country(**data) directly?"

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 raw = [{"name": "Kenya", "region": "Africa", "population": 54000000}, ...], which builds a list of Country instances from it?

Recall Session 09: **r spreads each dictionary's key-value pairs as keyword arguments into the Country constructor โ€” exactly matching __init__'s parameter names.
Question 2 of 5

What does @classmethod do to a method like from_dict?

A classmethod is bound to the class, not a specific instance. Its first parameter, cls, refers to the class itself โ€” useful for alternate constructors, since you don't have an instance yet at the point you're trying to build one.
Question 3 of 5

Given @classmethod\ndef from_dict(cls, data):\n return cls(**data), what does Country.from_dict({"name": "Kenya", "region": "Africa", "population": 54000000}) return?

Inside a classmethod, cls IS the class (Country), so cls(**data) is equivalent to calling Country(**data) โ€” constructing a new instance from the dictionary's keys and values.
Question 4 of 5

Why prefer a from_dict classmethod over just calling Country(**d) directly everywhere you need to convert a dict?

If the raw data's shape changes later (a renamed key, a new required default), you only need to update from_dict once, instead of hunting down every place Country(**d) was called directly.
Question 5 of 5

One raw record is missing the "population" key entirely. Using [Country.from_dict(r) for r in raw] with no error handling, what happens when that record is processed, assuming population has no default in __init__?

Without a default value or error handling, a missing required keyword argument raises a TypeError, which โ€” per Session 11 โ€” will propagate and stop the whole batch unless you wrap the conversion in a try/except to handle bad records individually.

9. Reflection Questions

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

  1. Why does cls(**data) still work correctly even if this classmethod were inherited by a subclass of Country (which we have not covered, but can you reason about it)?
  2. What would have happened if you used Country(**data) directly inside from_dict instead of cls(**data)? In this specific case, is there a difference?
  3. How does skipping one bad record instead of crashing the whole batch compare to the all-or-nothing behavior you might expect from a stricter language?
  4. Where in a real application would silently skipping bad records be dangerous instead of helpful? What would you do differently there?

10. What Breaks If This Knowledge Is Missing?

  • One bad record, whole batch lost: Without the try/except pattern from this session, a single malformed record anywhere in a large dataset would crash the entire conversion โ€” unacceptable for any real data source, which is never perfectly clean.
  • Object identity confusion (Session 19): The next session (the Layer 2 gate) asks whether two Country instances built from identical data are "the same" โ€” a question you can only reason about clearly once you are comfortable constructing many instances from data, as this session taught.
  • The mock data layer (Layer 4): Session 28's data-access layer is built almost entirely from the from_dict pattern in this session, applied to a whole file of mock JSON records.

11. What We Learned

Python concept mastered: Bulk-converting raw dicts into class instances with a from_dict classmethod, and skipping malformed records individually with try/except.

Unlocks: You can now turn any batch of raw dict-shaped data into a real list of working objects โ€” the exact operation every future data source will require.

Next session: Session 19 โ€” Object Identity and Equality. Layer 2 gate. We ask a subtle question: what does it mean for two objects to be "the same" โ€” identity vs equality.