Typorithm · medium

Object protocols and sequence behavior

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

Step 1 of 4 · intro card

Object protocols and sequence behavior

0%

Concept and contract

Implement a small immutable sequence while preserving normal indexing and iteration boundaries.

Python protocols describe operations an object supports, not merely its class name. A sequence should behave coherently for length, indexing, containment and traversal. Supplying a method with a familiar name is insufficient if its boundary behavior surprises callers.

Scores stores a tuple and implements the abstract Sequence requirements used here: __len__ and __getitem__. The mixin can derive other operations from those primitives. Delegation deliberately retains tuple indexing behavior, including negative indices, slices and IndexError at invalid integer positions, rather than inventing a different collection contract.

Mechanism and worked example

The constructor takes an immutable snapshot of the declared numeric input sequence.

The constructor takes an immutable snapshot of the declared numeric input sequence. Length delegates to that tuple, and indexing delegates the received index unchanged. The report catches an out-of-range access and records a label while independently measuring length and membership of zero.

For [4, 0, 8] and index minus one, the selected value is eight. The empty case and a positive out-of-range index exercise failure boundaries without mutating the source. Sequence mixins are convenient, but their derived algorithms may repeatedly call the primitives, so efficient implementations still need to consider their underlying access costs.

Complete worked example

python

from collections.abc import Sequence

class Scores(Sequence):
    def __init__(self, values):
        self._values = tuple(values)
    def __len__(self):
        return len(self._values)
    def __getitem__(self, index):
        return self._values[index]

def inspect_example(values, index):
    scores = Scores(values)
    try:
        selected = scores[index]
    except IndexError:
        selected = 'out-of-range'
    return {'item': selected, 'length': len(scores), 'containsZero': 0 in scores}
Reasoning
The constructor takes an immutable snapshot of the declared numeric input sequence. Length delegates to that tuple, and indexing delegates the received index unchanged. The report catches an out-of-range access and records a label while independently measuring length and membership of zero.
Download the reusable Python and packaging computer lab ↗

Counterexamples and common mistakes

The counterexample clamps negative indices to zero.

The counterexample clamps negative indices to zero. It appears defensive, yet changes a standard sequence operation: requesting the last element now returns the first. The acceptance case deliberately uses distinct first and last values to expose that semantic regression.

Returning None for every invalid index would also be dangerous because sequence traversal may rely on IndexError to establish an endpoint. Do not silently replace established protocol signals with arbitrary fallback values. If the domain needs clamped access, expose a separately named operation and document it instead of disguising it as ordinary indexing.

Controlled bug: the executable contract rejects this version

python

from collections.abc import Sequence

class Scores(Sequence):
    def __init__(self, values):
        self._values = tuple(values)
    def __len__(self):
        return len(self._values)
    def __getitem__(self, index):
        return self._values[max(0, index)]

def inspect_example(values, index):
    scores = Scores(values)
    try:
        selected = scores[index]
    except IndexError:
        selected = 'out-of-range'
    return {'item': selected, 'length': len(scores), 'containsZero': 0 in scores}
Reasoning
The counterexample clamps negative indices to zero. It appears defensive, yet changes a standard sequence operation: requesting the last element now returns the first. The acceptance case deliberately uses distinct first and last values to expose that semantic regression.
Download the reusable Python and packaging computer lab ↗

Independent practice

Extend the report with the reversed sequence while preserving the existing selection, length and membership fields.

Extend the report with the reversed sequence while preserving the existing selection, length and membership fields. Use the sequence protocol rather than reversing the caller's list. The first hint is that reversed can use the existing length and indexing operations.

The second hint is to materialize the reversed iterator into a new list only at the report boundary. An empty sequence should yield an empty reversed list, and the single-item out-of-range selection should still report its separate failure label. Reversal must not repair, reinterpret or hide an invalid requested index.

Step 1: Delegate index and slice behavior to the tuple and expose reversed iteration in the practice report. 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 collections.abc import Sequence

class Scores(Sequence):
    def __init__(self, values):
        self._values = tuple(values)
    def __len__(self):
        return len(self._values)
    def __getitem__(self, index):
        return self._values[index]

def inspect_example(values, index):
    scores = Scores(values)
    try:
        selected = scores[index]
    except IndexError:
        selected = 'out-of-range'
    return {'item': selected, 'length': len(scores), 'containsZero': 0 in scores}

# Task: Delegate index and slice behavior to the tuple and expose reversed iteration in the practice report.
# Practice contract: Delegate index and slice behavior to the tuple and expose reversed iteration in the practice report.
Reasoning
Delegate index and slice behavior to the tuple and expose reversed iteration in the practice report.
Download the reusable Python and packaging computer lab ↗
  • Hint 1: Extend the report with the reversed sequence while preserving the existing selection, length and membership fields. Use the sequence protocol rather than reversing the caller's list. The first hint is that reversed can use the existing length and indexing operations.
  • Hint 2: The second hint is to materialize the reversed iterator into a new list only at the report boundary. An empty sequence should yield an empty reversed list, and the single-item out-of-range selection should still report its separate failure label. Reversal must not repair, reinterpret or hide an invalid requested index.

Solution, tests and trade-offs

The solution uses list(reversed(scores)) and leaves both primitive methods unchanged.

The solution uses list(reversed(scores)) and leaves both primitive methods unchanged. Acceptance therefore checks a new derived operation against the same protocol implementation. It also verifies that reversed output does not mutate input values or change the independent indexing observation.

Inheritance from an abstract collection can provide reusable behavior and explicit requirements, while structural use may be enough for a small internal consumer. In either case, test the operation semantics you promise. A class passing an isinstance check is not evidence that all index boundaries, complexity assumptions or mutation rules are appropriate for its callers.

Practice solution and acceptance checks

python

from collections.abc import Sequence

class Scores(Sequence):
    def __init__(self, values):
        self._values = tuple(values)
    def __len__(self):
        return len(self._values)
    def __getitem__(self, index):
        return self._values[index]

def inspect_example(values, index):
    scores = Scores(values)
    try:
        selected = scores[index]
    except IndexError:
        selected = 'out-of-range'
    return {'item': selected, 'length': len(scores), 'containsZero': 0 in scores, 'reversed': list(reversed(scores))}
Reasoning
The solution uses list(reversed(scores)) and leaves both primitive methods unchanged. Acceptance therefore checks a new derived operation against the same protocol implementation. It also verifies that reversed output does not mutate input values or change the independent indexing observation.
Download the reusable Python and packaging computer lab ↗

Interview reasoning and next step

Explain why clamping an index can be a bug even when it avoids an exception.

Explain why clamping an index can be a bug even when it avoids an exception. Then identify which primitive operations enable the derived reversed traversal in this example. Distinguish a convenient mixin from a proof of application correctness.

For a stretch, add a slice case and describe the returned representation: this implementation delegates slices directly to tuple, rather than creating another Scores instance. Compare that choice with an explicit wrapper-preserving slice design. Write the intended contract before modifying __getitem__, because either representation can be valid when callers understand its boundaries.

Optional stretch — after the core task works: For a stretch, add a slice case and describe the returned representation: this implementation delegates slices directly to tuple, rather than creating another Scores instance. Compare that choice with an explicit wrapper-preserving slice design. Write the intended contract before modifying __getitem__, because either representation can be valid when callers understand its boundaries.

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-protocols.py and run --practice pye-protocols. 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.

Implement a small immutable sequence while preserving normal indexing and iteration boundaries.