Typorithm · medium

Frozen records, equality and hashable field values

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

Step 1 of 4 · intro card

Frozen records, equality and hashable field values

0%

Concept and contract

Separate blocked field assignment from deep immutability and valid hash keys.

A frozen dataclass blocks ordinary assignment to its fields. It does not recursively freeze referenced objects, validate annotations or make every possible field value hashable. An immutable record design therefore needs deliberate choices for both the class and its stored values.

Report stores a tuple of strings created from caller input. Tuple conversion establishes an owned immutable outer sequence for this declared input domain. The example also uses slots, which changes instance storage; slots is not the feature that supplies frozen assignment behavior or generated value equality.

Mechanism and worked example

The worked program builds one Report, changes a separate list copy and then attempts an ordinary field reassignment.

The worked program builds one Report, changes a separate list copy and then attempts an ordinary field reassignment. The FrozenInstanceError branch records that reassignment was blocked. Comparing with a second Report constructed from the same labels checks generated equality independently.

The returned labels remain in their original order, including duplicates. The verifier additionally compares caller inputs before and after execution, so modifying the original list would fail even if the returned record looked correct. No hash number is exposed: process-dependent hash values would make a poor prepared-result contract.

Complete worked example

python

from dataclasses import dataclass, FrozenInstanceError

@dataclass(frozen=True, slots=True)
class Report:
    labels: tuple[str, ...]

def inspect_example(labels):
    record = Report(tuple(labels))
    labels_copy = list(labels)
    labels_copy.append('later')
    try:
        record.labels = ('changed',)
    except FrozenInstanceError:
        blocked = True
    else:
        blocked = False
    return {'labels': list(record.labels), 'reassignmentBlocked': blocked,
            'equal': record == Report(tuple(labels))}
Reasoning
The worked program builds one Report, changes a separate list copy and then attempts an ordinary field reassignment. The FrozenInstanceError branch records that reassignment was blocked. Comparing with a second Report constructed from the same labels checks generated equality independently.
Download the reusable Python and packaging computer lab ↗

Counterexamples and common mistakes

The counterexample disables frozen while retaining slots and the tuple annotation.

The counterexample disables frozen while retaining slots and the tuple annotation. The attempted reassignment now succeeds and the record contains changed instead of the input labels. This demonstrates why neither an annotation nor slotted storage establishes the original assignment boundary.

A different failure would keep frozen but store a mutable list. The field could resist rebinding while list contents still changed, and hashing would encounter an unhashable field. Avoid unsafe_hash as a reflexive fix: a hash key whose equality-relevant value can change can violate collection invariants even if a hash method exists.

Controlled bug: the executable contract rejects this version

python

from dataclasses import dataclass, FrozenInstanceError

@dataclass(frozen=False, slots=True)
class Report:
    labels: tuple[str, ...]

def inspect_example(labels):
    record = Report(tuple(labels))
    labels_copy = list(labels)
    labels_copy.append('later')
    try:
        record.labels = ('changed',)
    except FrozenInstanceError:
        blocked = True
    else:
        blocked = False
    return {'labels': list(record.labels), 'reassignmentBlocked': blocked,
            'equal': record == Report(tuple(labels))}
Reasoning
The counterexample disables frozen while retaining slots and the tuple annotation. The attempted reassignment now succeeds and the record contains changed instead of the input labels. This demonstrates why neither an annotation nor slotted storage establishes the original assignment boundary.
Download the reusable Python and packaging computer lab ↗

Independent practice

Add a unique field to the report that counts a set containing the record and an equal second record.

Add a unique field to the report that counts a set containing the record and an equal second record. Preserve the reassignment check and label output. The expected count is one for empty, repeated and distinct labels because each pair has equal field values.

The first hint is to use the actual records as set elements rather than serializing them to strings. The second is to keep the tuple field and frozen configuration, which allow a compatible generated hash for this class. Do not assert a particular numeric hash or confuse two equal instances with one shared object identity.

Step 1: Use an immutable field value, then prove equal frozen records deduplicate as set keys. Step 2: Run the accepted baseline, edit the exercise, then check its independent solution cases; preserve caller inputs and the documented exception behavior.

Practice starter: solve before reading the solution

python

from dataclasses import dataclass, FrozenInstanceError

@dataclass(frozen=True, slots=True)
class Report:
    labels: tuple[str, ...]

def inspect_example(labels):
    record = Report(tuple(labels))
    labels_copy = list(labels)
    labels_copy.append('later')
    try:
        record.labels = ('changed',)
    except FrozenInstanceError:
        blocked = True
    else:
        blocked = False
    return {'labels': list(record.labels), 'reassignmentBlocked': blocked,
            'equal': record == Report(tuple(labels))}

# Task: Use an immutable field value, then prove equal frozen records deduplicate as set keys.
# Practice contract: Use an immutable field value, then prove equal frozen records deduplicate as set keys.
Reasoning
Use an immutable field value, then prove equal frozen records deduplicate as set keys.
Download the reusable Python and packaging computer lab ↗
  • Hint 1: Add a unique field to the report that counts a set containing the record and an equal second record. Preserve the reassignment check and label output. The expected count is one for empty, repeated and distinct labels because each pair has equal field values.
  • Hint 2: The first hint is to use the actual records as set elements rather than serializing them to strings. The second is to keep the tuple field and frozen configuration, which allow a compatible generated hash for this class. Do not assert a particular numeric hash or confuse two equal instances with one shared object identity.

Solution, tests and trade-offs

The accepted solution constructs the two records independently and lets a set deduplicate them.

The accepted solution constructs the two records independently and lets a set deduplicate them. Equality and hashing agree for these tuple-of-string fields, while ordinary reassignment remains blocked. Acceptance checks the observable collection size rather than implementation-specific memory addresses or hash values.

This remains a conventionally frozen Python object, not a claim of absolute immutability against every reflective operation. For application design, document the supported mutation boundary and validate values where they enter. If nested values are mutable, choose immutable representations or an explicit ownership strategy instead of relying on the decorator name.

Practice solution and acceptance checks

python

from dataclasses import dataclass, FrozenInstanceError

@dataclass(frozen=True, slots=True)
class Report:
    labels: tuple[str, ...]

def inspect_example(labels):
    record = Report(tuple(labels))
    labels_copy = list(labels)
    labels_copy.append('later')
    try:
        record.labels = ('changed',)
    except FrozenInstanceError:
        blocked = True
    else:
        blocked = False
    return {'labels': list(record.labels), 'reassignmentBlocked': blocked,
            'equal': record == Report(tuple(labels)), 'unique': len({record, Report(tuple(labels))})}
Reasoning
The accepted solution constructs the two records independently and lets a set deduplicate them. Equality and hashing agree for these tuple-of-string fields, while ordinary reassignment remains blocked. Acceptance checks the observable collection size rather than implementation-specific memory addresses or hash values.
Download the reusable Python and packaging computer lab ↗

Interview reasoning and next step

Explain why frozen and slots answer different questions.

Explain why frozen and slots answer different questions. Then distinguish field rebinding, mutation inside a referenced container, equality of two records and identity of one object. A clear explanation should predict the returned labels and the set size separately.

For a stretch, add another immutable field and decide whether it belongs in equality and hashing. Compare a tuple-of-strings field with a deliberately nested mutable value in an owned experiment. Record which operation fails or remains possible; avoid generalizing a successful hash of one record to every possible dataclass configuration.

Optional stretch — after the core task works: For a stretch, add another immutable field and decide whether it belongs in equality and hashing. Compare a tuple-of-strings field with a deliberately nested mutable value in an owned experiment. Record which operation fails or remains possible; avoid generalizing a successful hash of one record to every possible dataclass configuration.

Computer practice: https://applydjinn.com/typorithm/python-engineering-lab-20261107.zip. Use an isolated Python 3.13 environment, install requirements.txt, then run verify.py. Edit exercises/pye-records.py and run --practice pye-records. Fixture files are created in fresh temporary directories for every case. Packaging checks build real local artifacts, install the exact wheel without an index and run outside the source tree. No package is published to PyPI. The app includes static examples, prepared results and questions; it does not run Python or provide an editor.

Separate blocked field assignment from deep immutability and valid hash keys.