Typorithm · medium

Structural typing without imaginary runtime enforcement

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

Step 1 of 4 · intro card

Structural typing without imaginary runtime enforcement

0%

Concept and contract

Define a Protocol for consumers and verify good and bad implementations with a real type checker.

A Protocol describes the operations a statically checked consumer needs. Buffer can satisfy Sink through a compatible write method without explicitly inheriting from it. This separates an interface from a particular implementation and keeps the consumer small.

Type annotations do not automatically validate runtime values or guarantee an implementation's behavior. The owned lab therefore performs two different checks: actual sends must return the declared character count and text, while mypy checks a compatible implementation and rejects a deliberately incompatible method signature. Neither check substitutes for the other.

Mechanism and worked example

Sink declares a write operation accepting str and returning int.

Sink declares a write operation accepting str and returning int. Buffer appends the received string and returns len(text); send forwards exactly one call through that interface. The report returns both the count and the joined stored text.

The cases include an empty string and an accented character. This contract counts Python string characters, not encoded UTF-8 bytes, so the accented example returns one. A real byte-oriented sink would need a different input and count contract. Clear naming and acceptance cases prevent that distinction from being lost behind a generic write method.

Complete worked example

python

from typing import Protocol

class Sink(Protocol):
    def write(self, text: str) -> int: ...

class Buffer:
    def __init__(self) -> None:
        self.parts: list[str] = []
    def write(self, text: str) -> int:
        self.parts.append(text)
        return len(text)

def send(sink: Sink, text: str) -> int:
    return sink.write(text)

def inspect_example(text):
    buffer = Buffer()
    count = send(buffer, text)
    return {'count': count, 'text': ''.join(buffer.parts)}
Reasoning
Sink declares a write operation accepting str and returning int. Buffer appends the received string and returns len(text); send forwards exactly one call through that interface. The report returns both the count and the joined stored text.
Download the reusable Python and packaging computer lab ↗

Focused decision: use the complete example for context

python

class Sink(Protocol):
    def write(self, text: str) -> int: ...

def send(sink: Sink, text: str) -> int:
    return sink.write(text)
Decision
Send a newline-terminated message through the same structural interface and report the actual character count.
Download the reusable Python and packaging computer lab ↗

Counterexamples and common mistakes

The controlled bug returns zero from Buffer.write after appending the text.

The controlled bug returns zero from Buffer.write after appending the text. Its signature still looks compatible, but the observed count is wrong for nonempty input. A static checker cannot infer every semantic relationship between a method's side effects and return value.

Conversely, a WrongBuffer that accepts int and returns str does not satisfy this protocol's signature. The lab runs mypy on that separate negative fixture and requires an incompatibility diagnostic. Keeping semantic and structural failures separate avoids the false claim that successful annotation checking proves runtime business behavior.

Controlled bug: the executable contract rejects this version

python

from typing import Protocol

class Sink(Protocol):
    def write(self, text: str) -> int: ...

class Buffer:
    def __init__(self) -> None:
        self.parts: list[str] = []
    def write(self, text: str) -> int:
        self.parts.append(text)
        return 0

def send(sink: Sink, text: str) -> int:
    return sink.write(text)

def inspect_example(text):
    buffer = Buffer()
    count = send(buffer, text)
    return {'count': count, 'text': ''.join(buffer.parts)}
Reasoning
The controlled bug returns zero from Buffer.write after appending the text. Its signature still looks compatible, but the observed count is wrong for nonempty input. A static checker cannot infer every semantic relationship between a method's side effects and return value.
Download the reusable Python and packaging computer lab ↗

Independent practice

Send the message with one trailing newline and report the actual count and stored text after that operation.

Send the message with one trailing newline and report the actual count and stored text after that operation. Keep the same Sink interface and compatible Buffer implementation. The first hint is to change the value forwarded to send, rather than manually adjusting only the count.

The second hint is to test the empty string: it should now produce a newline and count one. For the accented input, the result has two characters. Do not change this into a byte-counting exercise or make the sink append another hidden newline, which would duplicate responsibility for formatting.

Step 1: Send a newline-terminated message through the same structural interface and report the actual character count. 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 typing import Protocol

class Sink(Protocol):
    def write(self, text: str) -> int: ...

class Buffer:
    def __init__(self) -> None:
        self.parts: list[str] = []
    def write(self, text: str) -> int:
        self.parts.append(text)
        return len(text)

def send(sink: Sink, text: str) -> int:
    return sink.write(text)

def inspect_example(text):
    buffer = Buffer()
    count = send(buffer, text)
    return {'count': count, 'text': ''.join(buffer.parts)}

# Task: Send a newline-terminated message through the same structural interface and report the actual character count.
# Practice contract: Send a newline-terminated message through the same structural interface and report the actual character count.
Reasoning
Send a newline-terminated message through the same structural interface and report the actual character count.
Download the reusable Python and packaging computer lab ↗
  • Hint 1: Send the message with one trailing newline and report the actual count and stored text after that operation. Keep the same Sink interface and compatible Buffer implementation. The first hint is to change the value forwarded to send, rather than manually adjusting only the count.
  • Hint 2: The second hint is to test the empty string: it should now produce a newline and count one. For the accented input, the result has two characters. Do not change this into a byte-counting exercise or make the sink append another hidden newline, which would duplicate responsibility for formatting.

Solution, tests and trade-offs

The solution appends the newline at the sending boundary and obtains the count from the sink's actual return value.

The solution appends the newline at the sending boundary and obtains the count from the sink's actual return value. Acceptance compares both fields, so adding one to an incorrect count without changing the text cannot satisfy the task.

The compatible and incompatible mypy fixtures make the static interface claim executable in the pinned lab. Runtime data boundaries still need explicit validation when values come from untrusted JSON or loosely typed callers. Protocols help organize a checked codebase; they do not turn arbitrary runtime objects into validated implementations through annotation alone.

Practice solution and acceptance checks

python

from typing import Protocol

class Sink(Protocol):
    def write(self, text: str) -> int: ...

class Buffer:
    def __init__(self) -> None:
        self.parts: list[str] = []
    def write(self, text: str) -> int:
        self.parts.append(text)
        return len(text)

def send(sink: Sink, text: str) -> int:
    return sink.write(text)

def inspect_example(text):
    buffer = Buffer()
    count = send(buffer, text + "\n")
    return {'count': count, 'text': ''.join(buffer.parts)}
Reasoning
The solution appends the newline at the sending boundary and obtains the count from the sink's actual return value. Acceptance compares both fields, so adding one to an incorrect count without changing the text cannot satisfy the task.
Download the reusable Python and packaging computer lab ↗

Interview reasoning and next step

Explain a program that passes type checking but fails this acceptance suite, using the zero-count counterexample.

Explain a program that passes type checking but fails this acceptance suite, using the zero-count counterexample. Then explain the opposite structural problem in WrongBuffer. Name the separate evidence needed for method compatibility and correct behavior.

For a stretch, introduce a generic protocol for another owned sink and examine how parameter and return types constrain substitution. Run both a compatible and an incompatible example through the checker. Keep runtime tests for real output, and avoid claiming that runtime_checkable would validate method signatures or the semantics of a write result.

Optional stretch — after the core task works: For a stretch, introduce a generic protocol for another owned sink and examine how parameter and return types constrain substitution. Run both a compatible and an incompatible example through the checker. Keep runtime tests for real output, and avoid claiming that runtime_checkable would validate method signatures or the semantics of a write result.

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

Define a Protocol for consumers and verify good and bad implementations with a real type checker.