Typorithm · medium

Classes, dataclasses and runtime contracts

pythonscriptingdata-boundariesindependent-practice
Saved on this device
Full lesson · 4 learning steps Open any exercise for focused practice
1

Step 1 of 4 · intro card

Classes, dataclasses and runtime contracts

0%

Concept and contract

Use a small data object without confusing type hints, generated methods and deep immutability.

A class defines behavior and data for its instances. The worked Role dataclass declares an id and title, receives generated initialization and comparison behavior, and adds a label method using self. The public describe function validates unknown input before constructing this small record.

Type annotations describe intended field types for readers and tools; ordinary dataclass construction does not automatically validate arbitrary external values against those hints. The explicit boundary therefore rejects missing, blank or non-string fields before an instance enters the rest of the program.

Mechanism and worked example

The frozen option blocks ordinary field reassignment on the created instance.

The frozen option blocks ordinary field reassignment on the created instance. This record contains only strings, making its chosen flat snapshot behavior easy to reason about. The lab actually attempts an ordinary id assignment and requires FrozenInstanceError rather than inferring the property from unchanged printed values.

Frozen is not a security boundary or a universal deep-freeze operation. A nested mutable collection could still change, and deliberate low-level mechanisms have different powers. Keep the promised object model small and distinguish normal API discipline from isolation against hostile code.

Complete worked example

python

from dataclasses import dataclass

@dataclass(frozen=True)
class Role:
    id: str
    title: str

    def label(self):
        return f"{self.id}: {self.title}"

def describe(raw):
    if not isinstance(raw, dict):
        raise ValueError("Expected a record")
    values = [raw.get("id"), raw.get("title")]
    if not all(isinstance(value, str) and value.strip() for value in values):
        raise ValueError("Expected nonempty strings")
    role = Role(values[0].strip(), values[1].strip())
    return {"id": role.id, "label": role.label()}
Reasoning
Runtime validation precedes construction; a separate probe checks ordinary frozen-field behavior.

Counterexamples and common mistakes

The counterexample removes frozen=True.

The counterexample removes frozen=True. Its describe output still looks correct, so return-value tests alone cannot show the contract violation. The reassignment probe exposes the change. Another common mistake assumes annotated fields reject integers automatically while accepting them directly from unknown JSON.

Generated dataclass methods reduce boilerplate; they do not choose a domain validation policy or make every object-oriented design appropriate. If a plain function and mapping express a small transformation clearly, a class should earn its complexity through a useful contract.

Counterexample: a plausible but incorrect implementation

python

from dataclasses import dataclass

@dataclass
class Role:
    id: str
    title: str

    def label(self):
        return f"{self.id}: {self.title}"

def describe(raw):
    if not isinstance(raw, dict):
        raise ValueError("Expected a record")
    values = [raw.get("id"), raw.get("title")]
    if not all(isinstance(value, str) and value.strip() for value in values):
        raise ValueError("Expected nonempty strings")
    role = Role(values[0].strip(), values[1].strip())
    return {"id": role.id, "label": role.label()}
Reasoning
Identical printed values do not prove an object still rejects reassignment.

Independent practice: small Python task

Keep validation and the frozen record, but change its label format to title followed by the id in square brackets.

Keep validation and the frozen record, but change its label format to title followed by the id in square brackets. For id r1 and title QA, predict QA [r1]. This is a small instance-method change that does not require a new inheritance hierarchy or additional fields.

Preserve the public describe result keys and the stripped id. The first step is editing the label method; the second is verifying that invalid inputs and ordinary reassignment still fail under the original boundary. Output presentation and object invariants remain separate checks.

Step 1: Edit the Role.label method. Step 2: Retain validation, stripping and the frozen decorator.

Practice starter: solve before reading the solution

python

from dataclasses import dataclass

@dataclass(frozen=True)
class Role:
    id: str
    title: str

    def label(self):
        # Return title followed by [id].
        raise NotImplementedError

def describe(raw):
    if not isinstance(raw, dict):
        raise ValueError("Expected a record")
    values = [raw.get("id"), raw.get("title")]
    if not all(isinstance(value, str) and value.strip() for value in values):
        raise ValueError("Expected nonempty strings")
    role = Role(values[0].strip(), values[1].strip())
    return {"id": role.id, "label": role.label()}
Reasoning
Change the instance method without weakening validation or the frozen contract.
  • Hint 1: The new output is title followed by a space and [id].
  • Hint 2: Do not replace describe with a raw unvalidated string formatter.

Solution, tests and trade-offs

The solution changes only the method's display format.

The solution changes only the method's display format. It keeps the same dataclass fields, frozen setting and unknown-input validation. The acceptance checks compare id and label while independently verifying the record rejects ordinary reassignment. A correct new label alone is not enough.

The input dictionary is also checked unchanged. Constructing a normalized record should not silently rewrite caller-owned source data. If future work needs mutable lifecycle behavior, design that state transition explicitly instead of removing frozen without revisiting the callers' assumptions.

Practice solution and acceptance checks

python

from dataclasses import dataclass

@dataclass(frozen=True)
class Role:
    id: str
    title: str

    def label(self):
        return f"{self.title} [{self.id}]"

def describe(raw):
    if not isinstance(raw, dict):
        raise ValueError("Expected a record")
    values = [raw.get("id"), raw.get("title")]
    if not all(isinstance(value, str) and value.strip() for value in values):
        raise ValueError("Expected nonempty strings")
    role = Role(values[0].strip(), values[1].strip())
    return {"id": role.id, "label": role.label()}
Reasoning
Labels change while the returned identity, input preservation and frozen behavior remain intact.

Interview reasoning and next step

In an interview, explain self, generated initialization, annotations and frozen behavior as separate mechanisms.

In an interview, explain self, generated initialization, annotations and frozen behavior as separate mechanisms. Give a nested-list counterexample to an overly broad immutability claim. These distinctions help you choose between a mapping, a data record and a richer behavioral object.

Next, write and read a small Unicode file with explicit resource management. An object's lifetime and a file handle's lifetime are different responsibilities; a context manager makes the latter boundary observable and reliable.

Optional stretch — after the core task works: Add a nested mutable member in a disposable example and show the difference between frozen field binding and deep immutability.

Computer practice: https://applydjinn.com/typorithm/python-lab-20261105.zip. Run verify.py with your selected Python interpreter, then complete an exercise and check it with --practice followed by the lesson id. The lab uses only the standard library and disposable local fixtures; it never connects to the production database or company feeds.

Use a small data object without confusing type hints, generated methods and deep immutability.