Layer 4 Session 30 โ€” Gate Mock Data

Designing Data Contracts

This is the Layer 4 gate. We formalize the informal "contract" from Session 27 using type hints and dataclasses, catching shape mismatches automatically instead of hoping.

OK name: str, region: str, population: int

An agreed, enforced shape every record must honor.

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:

  • Add type hints to a function or method signature
  • Define a class using @dataclass instead of a hand-written __init__
  • Explain what type hints do and do not enforce at runtime
  • Validate that a raw dict matches the expected contract before constructing an instance from it
  • Compare the dataclass version of Country to the hand-written version from Session 13

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 def summary(country: dict) -> str:, does Python prevent you from calling summary(42) at runtime?

This is a critical fact about Python type hints: they are NOT enforced by the language at runtime. They document intent and enable external tools (editors, mypy) to catch mismatches ahead of time, but summary(42) would still run and likely fail inside the function body instead.
Question 2 of 5

What does @dataclass generate automatically for a class, compared to the hand-written __init__ from Session 13?

@dataclass inspects the class's field declarations and auto-generates __init__ (and by default __eq__, among others) โ€” eliminating the repetitive self.x = x boilerplate from Session 13, while still producing an ordinary class.
Question 3 of 5

Given @dataclass\nclass Country:\n name: str\n region: str\n population: int, does this dataclass automatically give you the Session 19 value-based __eq__ behavior?

This directly connects back to Session 19: a dataclass's default __eq__ compares all declared fields, exactly like the __eq__ we wrote by hand โ€” but generated automatically, for free.
Question 4 of 5

A raw dict from a JSON file is missing the "population" key. If you call Country(**raw_dict) where Country is a dataclass requiring population, what happens?

Dataclasses generate a real __init__ under the hood โ€” type hints alone do not enforce anything, but the generated constructor still requires every field without a default, exactly like Session 13's hand-written version. This is why validating BEFORE construction (as this session's lab does) is still necessary for a clear, early error.
Question 5 of 5

Why would you validate that a raw dict has the correct keys and roughly correct types BEFORE passing it into a dataclass constructor, given that type hints are not enforced at runtime?

Since Python does not enforce type hints, a dataclass will happily accept Country(name="Kenya", region="Africa", population="fifty-four-million") without complaint at construction time โ€” the bug would only surface later, confusingly, wherever population is actually used as a number. Explicit validation (Session 24's discipline) catches this immediately, with a clear message.

3. The Concept โ€” Data Contracts with Type Hints and Dataclasses

FIELD DECLARATIONSname: strregion: strpopulation: int@DATACLASS GENERATES__init__, __eq__

@dataclass reads the field declarations and generates __init__ and __eq__ for you โ€” the same result as Sessions 09 and 15, without the boilerplate.

Type hints โ€” documentation, not enforcement

A type hint tells readers (and tools like editors and type checkers) what type a value is expected to be. Python itself does not check this at runtime โ€” it is a critical fact to internalize, since it is easy to assume otherwise.

def summary(country: dict) -> str:
    return f"{country['name']} ({country['region']})"

# This "should" be wrong according to the type hint, but Python runs it anyway:
print(summary(42))
# TypeError happens INSIDE the function body (42 is not subscriptable),
# not because the type hint was checked and enforced upfront

@dataclass โ€” less boilerplate, same underlying class

A dataclass declares its fields with type hints, and Python auto-generates __init__ (and by default, __eq__) from those declarations โ€” directly replacing Session 13's hand-written boilerplate.

from dataclasses import dataclass

@dataclass
class Country:
    name: str
    region: str
    population: int

kenya = Country(name="Kenya", region="Africa", population=54000000)
print(kenya.name)        # "Kenya" โ€” same as before
print(kenya.population)  # 54000000

peru1 = Country(name="Peru", region="Americas", population=33000000)
peru2 = Country(name="Peru", region="Americas", population=33000000)
print(peru1 == peru2)  # True โ€” free, field-by-field equality (Session 19's manual __eq__, generated automatically)

Type hints still don't stop bad values at construction

A dataclass will happily accept a value of the wrong type โ€” the hint is not checked. This is why explicit validation, at the boundary where raw external data enters the program, is still necessary.

bad = Country(name="Kenya", region="Africa", population="fifty-four-million")
print(bad.population)  # "fifty-four-million" โ€” a string, accepted without complaint!
# This will fail confusingly later, wherever population is actually used as a number

Validating the contract before construction

Combining Session 24's validation discipline with the dataclass gives us both convenience AND safety: check the raw dict's shape and types explicitly, THEN construct.

def validate_country_record(data):
    required = {"name": str, "region": str, "population": int}
    for key, expected_type in required.items():
        if key not in data:
            raise ValueError(f"missing required field: {key}")
        if not isinstance(data[key], expected_type):
            raise TypeError(f"{key} must be {expected_type.__name__}, got {type(data[key]).__name__}")
    return data

def build_country(data):
    validate_country_record(data)
    return Country(**data)
Layer 4 gate: This is the last Layer 4 session. Every remaining layer assumes you understand that type hints document intent but do not enforce it, and that explicit validation is what actually protects the boundary between raw external data and your application.

4. Lab

Lab objective: Convert Country to a dataclass, prove type hints are not enforced, and add explicit contract validation for raw records loaded from JSON.

What you will build

A file called contracts_lab.py.

Step-by-step instructions

1

Create the file with Country as a dataclass

# contracts_lab.py
from dataclasses import dataclass

@dataclass
class Country:
    name: str
    region: str
    population: int

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

Prove the free __eq__ works, connecting back to Session 19

a = Country(name="Kenya", region="Africa", population=54000000)
b = Country(name="Kenya", region="Africa", population=54000000)
print("a == b:", a == b)   # True โ€” generated automatically
print("a is b:", a is b)   # False โ€” still separate objects
3

Prove type hints are not enforced at construction

bad = Country(name="Kenya", region="Africa", population="not a number")
print(bad.population)              # accepted anyway!
print(type(bad.population))        # <class 'str'> โ€” the hint did nothing to stop this
4

Write validate_country_record() checking both presence and type

def validate_country_record(data):
    required = {"name": str, "region": str, "population": int}
    for key, expected_type in required.items():
        if key not in data:
            raise ValueError(f"missing required field: {key}")
        if not isinstance(data[key], expected_type):
            raise TypeError(f"{key} must be {expected_type.__name__}, got {type(data[key]).__name__}")
    return data
5

Validate a batch of raw records, including a deliberately bad one, and skip failures gracefully

Reuse the Session 18 pattern of catching per-record errors without losing the whole batch.

raw_records = [
    {"name": "Kenya", "region": "Africa", "population": 54000000},
    {"name": "Bad Data", "region": "Africa", "population": "fifty"},  # wrong type
    {"name": "Also Bad", "region": "Africa"},                          # missing key
]

good_countries = []
for r in raw_records:
    try:
        validate_country_record(r)
        good_countries.append(Country(**r))
    except (ValueError, TypeError) as e:
        print(f"Rejected {r!r}: {e}")

print("Valid countries:", len(good_countries))

5. Expected Files Changed

FileActionWhy
contracts_lab.py Created Converts Country to a dataclass and adds explicit contract validation for raw records.
docs/sessions/session-30/index.html Created This session document โ€” Layer 4 gate.
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 contracts_lab.py docs/sessions/session-30/index.html
git commit -m "session-30: formalize the country data contract with dataclasses and explicit validation"
Do not commit until you can answer out loud: "Why does population="not a number" get accepted by Country(**data) even though population is type-hinted as int?"

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 def summary(country: dict) -> str:, does Python prevent you from calling summary(42) at runtime?

This is a critical fact about Python type hints: they are NOT enforced by the language at runtime. They document intent and enable external tools (editors, mypy) to catch mismatches ahead of time, but summary(42) would still run and likely fail inside the function body instead.
Question 2 of 5

What does @dataclass generate automatically for a class, compared to the hand-written __init__ from Session 13?

@dataclass inspects the class's field declarations and auto-generates __init__ (and by default __eq__, among others) โ€” eliminating the repetitive self.x = x boilerplate from Session 13, while still producing an ordinary class.
Question 3 of 5

Given @dataclass\nclass Country:\n name: str\n region: str\n population: int, does this dataclass automatically give you the Session 19 value-based __eq__ behavior?

This directly connects back to Session 19: a dataclass's default __eq__ compares all declared fields, exactly like the __eq__ we wrote by hand โ€” but generated automatically, for free.
Question 4 of 5

A raw dict from a JSON file is missing the "population" key. If you call Country(**raw_dict) where Country is a dataclass requiring population, what happens?

Dataclasses generate a real __init__ under the hood โ€” type hints alone do not enforce anything, but the generated constructor still requires every field without a default, exactly like Session 13's hand-written version. This is why validating BEFORE construction (as this session's lab does) is still necessary for a clear, early error.
Question 5 of 5

Why would you validate that a raw dict has the correct keys and roughly correct types BEFORE passing it into a dataclass constructor, given that type hints are not enforced at runtime?

Since Python does not enforce type hints, a dataclass will happily accept Country(name="Kenya", region="Africa", population="fifty-four-million") without complaint at construction time โ€” the bug would only surface later, confusingly, wherever population is actually used as a number. Explicit validation (Session 24's discipline) catches this immediately, with a clear message.

9. Reflection Questions

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

  1. Why do you think Python chose not to enforce type hints at runtime by default, when other languages do enforce their type systems? What tradeoff does this represent?
  2. How does validate_country_record() in this session compare to validate_population() from Session 24? What is genuinely new versus what is the same idea applied at a different level?
  3. If a real API someday returns population as a string like "54000000" (valid digits, but the wrong TYPE), would your current validate_country_record() accept or reject it? Is that the right behavior?
  4. How does the dataclass's generated __init__ relate back to everything you learned about __init__ and self in Session 13?

10. What Breaks If This Knowledge Is Missing?

  • Trusting type hints as enforcement: A very common and dangerous misconception is believing type hints protect you from bad data at runtime. This session should permanently correct that โ€” hints inform tooling and readers, but only explicit validation actually protects your program.
  • Testing the data layer (Session 35): The tests you write for CountryRepository in Layer 5 will directly exercise validate_country_record() with both good and bad records โ€” this session's validation logic IS what gets tested.
  • Real, messy API data (Layer 7): A real external API is far less trustworthy than your own mock or JSON data โ€” it can return unexpected types, missing fields, or malformed values at any time. The validation discipline from this session is what stands between that chaos and your application crashing.

11. What We Learned

Python concept mastered: Type hints as documentation (not enforcement), dataclasses as a concise way to define structured classes, and explicit validation as the real protection at data boundaries.

Unlocks: The Country data contract is now explicit, documented, and actually enforced โ€” the last Layer 4 skill before we start testing everything we've built.

Next session: Session 31 โ€” Why We Test and What to Test. Layer 5 begins. We start testing this application deliberately, instead of manually re-running scripts to check our work.