---
title: 'Object-Oriented Python and Types'
source: 'https://academia.sh/en/courses/python-object-and-types'
language: en
updated: '2026-08-17T04:42:23+00:00'
license: 'CC BY-SA 4.0'
lessons: 11
outcomes:
    - "Writes classes that fit the language's syntax through data model methods"
    - 'Distinguishes the cases where composition should be chosen over inheritance'
    - 'Makes contracts explicit with type hints and a static checker'
---

# Object-Oriented Python and Types

The number that describes a design is not what it does but who answers the call and who checks the contract; the course measures inheritance against composition through the body that answers, and the type layer through the violations the runtime, the checker, and the field rule each catch separately.

## Classes and the Data Model

Binding of attributes and methods, multiple inheritance's resolution order, visibility declared through naming, representation and comparison protocols, controlling attribute access, short definitions for value objects, and nominal versus structural contracts.

### Class and Instance

Source: https://academia.sh/en/courses/python-object-and-types/class-and-instance

An attribute written in the class body is shared as 1 object across three instances; one written in the instance body produces 3 separate objects. `self` is not a keyword — it is the instance carried by the bound method built at the moment of access.

The Data Structures and Functional Tools course counted a container's cost and closed on a
line: **choosing a container and writing one are separate things.** Every protocol measured
so far worked on objects the language already gave — a list had a length, a tuple could be a
key, a dict could be compared. None of it was a contract we wrote; each was a ready-made
property of a chosen container.

This course sits on the other side of that line: **we write the side that takes part in the
protocol.** And writing something does not mean it actually answers a call. The course has
one question: **who answers the call?** The first lesson builds the smallest form of that
question. When a class is defined, two separate namespaces come into being — the class
itself, and every instance built from it. When an attribute is read, which of the two does
the answer come from, and where is that name `self` born?

#### Two Separate Namespaces

When a `class` statement runs, the interpreter runs the body **once** and writes the names
produced in the body into the class object's dict. A class is an object that exists at
runtime; the methods, too, are ordinary functions sitting inside that dict.

Every instance, in turn, arrives with its **own** dict. When `self.name = ...` is written
inside `__init__`, the name is written not to the class but to that instance's dict. When an
attribute is read, the lookup checks the instance's dict first, then the class's, then the
class's ancestors. **Whoever is found first gives the answer.** The shared rig's
`answering_class` function writes the class side of this lookup out: it says which class
first carries a name during resolution.

`__init__`'s name follows from this arrangement too. When an instance call is made, an empty
object is built **first**, and `__init__` runs **on** that object **afterward** — so
`__init__` does not produce an object, it fills the dict of an object that has already been
built. This is why it returns nothing — it has no object to return, it has an object it was
given.

The measurement's assumptions:

- **CD1** — The measured class is the shared rig's `Record` class; a class attribute (`kind`)
  has been added to its body, and the `source` and `format` capabilities are unchanged.
- **CD2** — Three instances are built from the same class with a single comprehension; the
  only difference between them is the name given to `__init__`.
- **CD3** — The "separate object" column compares the value seen by the first two instances
  with `is`; **the identity number itself is not printed.**
- **CD4** — The "in instance dict" column counts how many instances carry the name in their
  **own** dict; names coming from the class do not count toward this number.

```python
"""Where an attribute stands: the class body versus the instance body."""


class Record:
    """The shared rig's base class; two capabilities are used here."""

    kind = "record"                       # written in the class body

    def __init__(self, name):
        self.name = name                  # written in the instance body

    def source(self):
        return "Record"

    def format(self):
        return f"<{self.source()}>"


def answering_class(cls, name):
    """Which class provides an attribute: the first one found during resolution."""
    for s in cls.__mro__:
        if name in s.__dict__:
            return s.__name__
    return None


trio = [Record(f"r{i}") for i in range(3)]

print(f"{'attribute':<10s} {'answering':>11s} {'separate object':>16s} "
      f"{'in instance dict':>17s}")
for name in ("kind", "name"):
    separate = getattr(trio[0], name) is not getattr(trio[1], name)
    local = sum(1 for t in trio if name in t.__dict__)
    print(f"{name:<10s} {answering_class(Record, name) or 'instance':>11s} {str(separate):>16s} "
          f"{local:>17d}")

a, b = Record("a"), Record("b")
print(f"\n{'at start':<19s}a.kind={a.kind!r} b.kind={b.kind!r} Record.kind={Record.kind!r} "
      f"kind in a's dict -> {'kind' in a.__dict__}")
a.kind = "custom"
print(f"{'a.kind assigned':<19s}a.kind={a.kind!r} b.kind={b.kind!r} Record.kind={Record.kind!r} "
      f"kind in a's dict -> {'kind' in a.__dict__}")
del a.kind
print(f"{'attribute deleted':<19s}a.kind={a.kind!r} b.kind={b.kind!r} Record.kind={Record.kind!r} "
      f"kind in a's dict -> {'kind' in a.__dict__}")
```

```
attribute    answering  separate object  in instance dict
kind            Record            False                 0
name          instance             True                 3

at start           a.kind='record' b.kind='record' Record.kind='record' kind in a's dict -> False
a.kind assigned    a.kind='custom' b.kind='record' Record.kind='record' kind in a's dict -> True
attribute deleted  a.kind='record' b.kind='record' Record.kind='record' kind in a's dict -> False
```

The first table shows the two names standing in two separate places. When `kind` is read,
the answer comes from **`Record`**; all three instances see the same object, and **none** of
them carries this name in its own dict — the count is **0**. When `name` is read, no class
answers; all three instances carry it in their own dict, the count is **3**, and the objects
they see are separate.

The bottom section is a direct consequence of lookup order. `a.kind = "custom"` does **not**
change the name on the class; it opens a new entry in `a`'s dict, and since that entry is
found first, it **shadows** the class's. `b` and the class stay exactly as they were. Once
the instance's name is deleted, the shadow lifts and `a.kind` reads the class's value again.
Assigning to an instance never
touches the class — **the only way to change the class is to write to the class.**

#### The Class Body Runs Once

The distinction above has a cost side, and it goes unnoticed in careless writing: the class
body runs **once**, at definition time; the instance body (`__init__`) runs **on every
instance**. Writing `= []` in the class body builds a single list, and every instance shares
that same list.

- **CD5** — The measured class deliberately defines two names of the same kind in two
  separate bodies; there is no difference between them other than the names.
- **CD6** — Three instances are built, and each one appends **its own index** to both
  containers; what is measured is how many items each instance sees at the end.
- **CD7** — Sharing is shown with `is`.

```python
class Shared:
    """Defines two names of the same kind in separate bodies: one in the class, one in the instance."""

    shared_box = []                     # class body: runs once

    def __init__(self):
        self.own_box = []          # instance body: runs per instance


trio = [Shared() for _ in range(3)]
for i, n in enumerate(trio):
    n.shared_box.append(i)
    n.own_box.append(i)

print(f"\n{'attribute':<14s} {'containers made':>16s} {'each instance sees':>20s} "
      f"{'shared':>13s}")
print(f"{'shared_box':<14s} {1:16d} {str([len(n.shared_box) for n in trio]):>20s} "
      f"{str(trio[0].shared_box is trio[2].shared_box):>13s}")
print(f"{'own_box':<14s} {3:16d} {str([len(n.own_box) for n in trio]):>20s} "
      f"{str(trio[0].own_box is trio[2].own_box):>13s}")
```

```
attribute       containers made   each instance sees        shared
shared_box                    1            [3, 3, 3]          True
own_box                       3            [1, 1, 1]         False
```

Three instances each made one append to a container. The class body's container was built
**1** time, so all three appends went into the same list, and every instance sees **3**
items. The instance body's container was built **3** times, and each instance sees only what
it appended itself — **1** item. The numbers come from the same rows; the only thing that
separates them is where the body sits.

A writing rule follows from this: **a mutable value is not written in the class body.** The
class body is for constants, numbers, and immutable values — for those, sharing has no
observable consequence, because no one can mutate them in place. Any state that belongs to
one instance is set up inside `__init__`. The correct use of a class attribute shows up in
the same table: `kind` in the first measurement is a **default**, and an instance only
shadows it when needed; one object is enough for three instances, because nobody mutates it
in place.

#### `self` Is Not a Keyword

An object sitting in the class dict is an ordinary function, and its first parameter is no
different from any other name. When accessed through an instance, a step is inserted: the
access produces a **bound method** object that holds both the function and the instance
together. This is why we do not write the first argument in the call — that argument is
already bound at the moment of access.

- **CD8** — The measurement continues with the first block's `Record` class; no new
  definition is made.
- **CD9** — The same call is made in two forms: through the instance, and through the class
  with the instance passed by hand. What is measured is whether the two forms give the
  **same result**.
- **CD10** — The objects' type is printed as a name; **identity numbers and memory data are
  not printed.**
- **CD11** — The call in the last line is deliberately made without an instance; the
  exception's **name** is recorded.

```python
k = Record("k")
print(f"\nk.format() -> {k.format()} | Record.format(k) -> {Record.format(k)} | "
      f"equal -> {k.format() == Record.format(k)}")
print(f"type of object in class dict -> "
      f"{type(Record.__dict__['format']).__name__}")
print(f"type of object accessed via instance -> {type(k.format).__name__}")
print(f"bound method holds instance k -> {k.format.__self__ is k}")
print(f"k.format is k.format -> {k.format is k.format} | "
      f"Record.format is Record.format -> {Record.format is Record.format}")
print(f"function wrapped by bound method -> {k.format.__func__ is Record.format}")

try:
    Record.format()
except TypeError as e:
    print(f"call without instance -> {type(e).__name__}")

print()
print(f"{'capability':<10s} {'answering class':>17s}")
for name in ("source", "format", "__init__"):
    print(f"{name:<10s} {answering_class(Record, name):>17s}")
print(f"{'name':<10s} {answering_class(Record, 'name') or '—':>17s}")
```

```
k.format() -> <Record> | Record.format(k) -> <Record> | equal -> True
type of object in class dict -> function
type of object accessed via instance -> method
bound method holds instance k -> True
k.format is k.format -> False | Record.format is Record.format -> True
function wrapped by bound method -> True
call without instance -> TypeError

capability   answering class
source                Record
format                Record
__init__              Record
name                       —
```

The first line shows the two forms doing the same job: `k.format()` gives the same result as
`Record.format(k)`. The second is the unfolded form of the first. The next two lines name
the step in between — the object sitting in the class dict is a **function**, the object
accessed through the instance is a **method**. The fourth line says what the method carries:
the instance the access was made through.

The fifth line proves that binding happens **at the moment of access**. Reading the same
name twice produces two separate bound-method objects; the `is` comparison gives `False`.
Accessing through the class involves no binding — the same function in the dict comes back
every time, and the comparison gives `True`. The sixth line ties the two together: the
function wrapped by the bound method is the **very same** function read from the class. This
is also why the instance-less call raises `TypeError` — the function expected a parameter,
and there was no instance to bind it.

This has two everyday consequences. First, a bound method carries as a **value** without
being called: passing `k.format` somewhere carries `k` with it, since the object holds the
instance inside itself — putting a method into a callback list puts the instance in too.
Second, a method can always be read off the class and called by hand; the instance is not a
hidden argument, it is an explicit, writable first one.

Object-oriented programming's concepts were built in the Programming Fundamentals course;
**they are not repeated here.** There, a class was a template and an instance one production
of it. What is measured here is that the template itself is also an **object**, and which
dict the answer comes from. The last table closes this: **`Record`** answers all four names,
and **no** class answers the `name` attribute — it is not written in any class, it is born
separately in every instance.

#### Three Forms of Binding

Since binding is a rule, it can be changed. Three separate binding forms can be defined in
the same class body: a method whose first argument is bound to the **instance**, one bound
to the **class**, and one bound to **nothing**. All three sit in the same body and are called
the same way; the only place they differ is what the first argument is at call time.

- **CD12** — Three methods are defined in a single class body, and all three do the same
  job: return the name of whatever is bound to them. If nothing is bound, a dash is returned
  in its place.
- **CD13** — The measurement is made through a **subclass**; the class that writes the bodies
  and the class the call is made on are deliberately separated.
- **CD14** — The "call" columns call the method **with no arguments**; a call that cannot be
  satisfied falls with an exception, and the **exception's name** is recorded with an
  exclamation prefix.
- **CD15** — The access columns print the returned object's type as a name; **identity
  numbers are not printed.**

```python
class Formatter:
    """Three binding forms in the same body: bound to the instance, the class, and nothing."""

    def instance_method(self):
        return type(self).__name__

    @classmethod
    def class_method(cls):
        return cls.__name__

    @staticmethod
    def static_method():
        return "—"


class SubFormatter(Formatter):
    pass


def attempt(call):
    try:
        return call()
    except TypeError as e:
        return f"!{type(e).__name__}"


n = SubFormatter()
print(f"\n{'method':<15s} {'class access':>15s} {'instance access':>15s} "
      f"{'class call':>15s} {'instance call':>15s}")
for name in ("instance_method", "class_method", "static_method"):
    print(f"{name:<15s} {type(getattr(SubFormatter, name)).__name__:>15s} "
          f"{type(getattr(n, name)).__name__:>15s} "
          f"{attempt(getattr(SubFormatter, name)):>15s} "
          f"{attempt(getattr(n, name)):>15s}")

print(f"\nclass that wrote the body {answering_class(SubFormatter, 'class_method')}, "
      f"class bound to cls {SubFormatter.class_method()}")
```

```
method             class access instance access      class call   instance call
instance_method        function          method      !TypeError    SubFormatter
class_method             method          method    SubFormatter    SubFormatter
static_method          function        function               —               —

class that wrote the body Formatter, class bound to cls SubFormatter
```

The table shows row by row where binding happens. The instance method is bound only when
accessed **through the instance**; the object accessed through the class is a bare function,
and calling it with no arguments falls with `TypeError`. The class method is bound on
**both** kinds of access — even read through the class it comes back as a method, because
the thing to bind is already the class itself; the no-argument call works either way. The
static method is bound on neither access; a bare function comes back both ways, and both
calls report that there is nothing bound.

**Five** of the six calls land, **one** falls. The falling call is not a gap, it is the rule
itself: an instance method cannot be called without an instance, because there is nothing to
fill its first parameter.

The last line is the first sharp form of the course's question. `class_method`'s body is
written in **`Formatter`**; but because the call is made through `SubFormatter`, the class
bound to `cls` is **`SubFormatter`**. The class that writes the body and the context that
answers the call are not the same thing — the body sits in one place, the binding happens at
call time. What the next lesson measures is this same distinction, enlarged.

#### Summary

- Class and instance are two separate namespaces; attribute lookup checks the instance's
  dict first, then the class's, and **whoever is found first gives the answer.**
- `kind` in the class body is shared as **1** object across three instances and is not found
  in any of their dicts; `name` inside `__init__` produces **3** separate objects and sits in
  all three dicts.
- Assigning to an instance does not change the class, it shadows it; once the instance's name
  is deleted, the class's value becomes visible again.
- The class body runs once, the instance body runs per instance: a container defined in the
  class is built **1** time and holds all **3** appends together; one defined in the instance
  is built **3** times.
- `self` is not a keyword: access through an instance produces a new **bound method** every
  time, that method carries the instance it was accessed through, and passes it as the first
  argument at call time.
- There are three forms of binding; **5** of six calls land, and the one that falls is an
  instance method called without an instance. A class method whose body is written in
  `Formatter`, when called through `SubFormatter`, binds **`SubFormatter`** to `cls`.

#### Next Step

In this lesson the one that always answered was `Record`, because there was only one class
and lookup ended on the first step. When classes are derived from each other, lookup gets
longer: a name may be written in more than one class, and **resolution order** decides which
one is found. The next lesson measures that order and pays off the course's second claim —
across an inheritance tree, which classes does the body of four capabilities spread across;
how many of the same four capabilities does a class built with composition write in its own
body; and why does a method whose body is written in one class print another class's name?

### Inheritance and Method Resolution Order

Source: https://academia.sh/en/courses/python-object-and-types/inheritance-and-method-resolution-order

The body of four capabilities spreads across three separate classes in inheritance, and `Document` writes 0 of them in its own body; `Wrapper` writes all 4 in composition. The `format` method, whose body is written in `Record`, prints `<Signed>`.

The previous lesson showed that in a single-class tree, the answer always comes from the
same place: lookup checks the instance's dict, moves to the class if it fails there, and
stops. The last measurement had already drawn the line — a class method whose body is
written in `Formatter`, called through `SubFormatter`, bound `SubFormatter` to `cls`. Where
the body sits and where the call is answered had already come apart.

This lesson enlarges that split and pays off the course's **second claim**. The same four
capabilities will be given in two separate designs: one an inheritance tree, the other
composition. Both designs produce the same results. The question is not the result: **which
class is each capability's body written in?** The question suits measurement, because its
answer comes from reading code, not opinion — a name either exists in a class's dict or it
does not.

#### Same Four Capabilities, Two Designs

The inheritance side is made of four classes. `Record` carries two capabilities: `source`,
and `format`, which calls it. `Timed` derives from it and adds `stamp`. `Signed` also derives
from it, adds `signature`, and **rewrites** `source`. `Document` derives from both and its
body is empty.

The composition side is a single class. `Wrapper` derives from nothing; it holds two objects
inside itself and writes all four capabilities in its own body, passing each call through to
the object inside it.

Which class a capability's body is written in is told by the **method resolution order
(MRO)**. Every class carries such an order: a list, computed once at definition time, of
which classes a lookup walks and in what sequence. The `answering_class` function walks this
list and returns the first class carrying the name.

`Document`'s empty body is not a gap, it is the measurement itself. That body declares
exactly one thing: which classes it derives from, and **in what order**. The class's entire
behavior is born from this single declaration, and when the declaration changes, so does the
behavior — reorder the bases and the class answering `source` changes; add one line to
`Document`'s body and the answer shifts there. Composition has no such declaration;
`Wrapper`'s behavior is read only from its own body.

The measurement's assumptions:

- **CD16** — The four-class tree and `Wrapper` are the shared rig's defined form; the
  placement of capabilities has not been changed.
- **CD17** — The four measured capabilities are `source`, `format`, `stamp`, and `signature`;
  the measurement looks at the **class the body is written in**, not the call's **result**.
- **CD18** — The composition column is not a constant — it would come out the same even read
  from `Wrapper`'s own dict: all four names are written in that body.
- **CD19** — When the resolution order is printed, the list's last item (the common base of
  all classes) is trimmed; what is measured is the rig's own four classes.

```python
"""Who answers the call: inheritance and composition side by side."""


class Record:
    def source(self):
        return "Record"

    def format(self):
        return f"<{self.source()}>"


class Timed(Record):
    def stamp(self):
        return "time"


class Signed(Record):
    def source(self):
        return "Signed"

    def signature(self):
        return "signature"


class Document(Timed, Signed):
    pass


class Wrapper:
    """An object giving the same capability via composition: who answers is written explicitly."""

    def __init__(self):
        self.timed = Timed()
        self.signed = Signed()

    def source(self):
        return self.signed.source()

    def stamp(self):
        return self.timed.stamp()

    def signature(self):
        return self.signed.signature()

    def format(self):
        return f"<{self.source()}>"


def answering_class(cls, name):
    """Which class provides an attribute: the first one found during resolution."""
    for s in cls.__mro__:
        if name in s.__dict__:
            return s.__name__
    return None


CAPABILITIES = ("source", "format", "stamp", "signature")

k = {name: answering_class(Document, name) for name in CAPABILITIES}
b = {name: "Wrapper" for name in CAPABILITIES}

print(f"{'capability':<10s} {'answering in inheritance':>25s} {'answering in composition':>25s}")
for name in CAPABILITIES:
    print(f"{name:<10s} {k[name]:>25s} {b[name]:>25s}")
print()
print("resolution order: " + " -> ".join(s.__name__ for s in Document.__mro__[:-1]))
print(f"Document().format() -> {Document().format()} | Wrapper().format() -> "
      f"{Wrapper().format()}")
print(f"in inheritance, capability with body written in Document: "
      f"{sum(1 for a in CAPABILITIES if k[a] == 'Document')}/{len(CAPABILITIES)}; "
      f"in composition: {sum(1 for a in CAPABILITIES if b[a] == 'Wrapper')}/{len(CAPABILITIES)}")
```

```
capability  answering in inheritance  answering in composition
source                        Signed                   Wrapper
format                        Record                   Wrapper
stamp                          Timed                   Wrapper
signature                     Signed                   Wrapper

resolution order: Document -> Timed -> Signed -> Record
Document().format() -> <Signed> | Wrapper().format() -> <Signed>
in inheritance, capability with body written in Document: 0/4; in composition: 4/4
```

The last line is the claim itself. `Document` carries **0** of the four capabilities in its
own body; `Wrapper` carries all **4**. Reading `Document`'s body shows exactly one thing —
two base names; what the object can do is **not written** in that body. Reading `Wrapper`'s
body shows all four capabilities and who each call is passed to.

The left column shows the answer spread across three separate classes: `source` comes from
`Signed`, `format` from `Record`, `stamp` from `Timed`. `source` is written in two classes at
once — both `Record` and `Signed` — but since `Signed` comes first in resolution order, it
gives the answer. The body in `Record` is still there and is never called.

The subtler point sits in the second row. `format`'s body is written in **`Record`**, and
that body writes `self.source()`. Since the body sits inside `Record`, it might seem it would
call the `source` there; **it does not.** The call is made through `self`, and `self` is a
`Document` instance — the lookup runs in `Document`'s order and stops at `Signed`. The result
comes out `<Signed>`. **Reading a body does not tell you what it will produce;** that is told
by the resolution order of the object at call time. `Wrapper` gives the same result, but
there the destination of the pass-through is written inside the body itself.

The Programming Paradigms course established preferring composition over inheritance as a
**design principle**, alongside the fragile base class problem; **that discussion is not
repeated here.** What is measured here is not the principle's justification but its
measurable counterpart: **where** the body is written. 0/4 against 4/4 is the same behavior's
two different readability costs.

The two numbers point in opposite directions, and the choice is made exactly here. Adding a
fifth capability costs **0** lines in inheritance — a method written on `Timed` shows up on
`Document` automatically. In composition it costs **1** line, a pass-through written on
`Wrapper`. In exchange, answering "who answers this call" costs reading four bodies and their
order in inheritance, one body in composition. Inheritance shortens the write and lengthens
the read; composition does the reverse.

The split has an object side too. A `Document` instance is **one** object; all four classes'
bodies operate on a single dict, and any state `Record` holds sits there **once**. A
`Wrapper` instance, by contrast, is **three** objects — itself and the two built in
`__init__` — and state from `Record` is held **separately** in each. Both designs answer the
same calls, but store state in a different number of places: sharing is unavoidable in
inheritance, and happens in composition only if written explicitly.

#### How the Order Is Built

The order `Document -> Timed -> Signed -> Record` is not arbitrary; it is produced by two
rules: every class comes **before** its own bases, and bases are kept in the **order they are
written** in the class body. `Record` sits last because it is the base of both `Timed` and
`Signed`, and both have to come before it.

These two rules cannot always be satisfied together. When they cannot, the class **cannot be
built**, and the error is raised not while an instance is built but while the `class`
statement itself runs.

- **CD20** — Four base orders are built from the same three classes; the built class itself
  is dropped from the output, only the order after it is printed.
- **CD21** — For an order that fails to build, the **exception's name** is recorded with an
  exclamation prefix.
- **CD22** — The classes are the previous block's definitions; the tree is unchanged.

```python
def try_building(bases):
    """Can a class be built with the given base order?"""
    try:
        s = type("Trial", bases, {})
    except TypeError as e:
        return f"!{type(e).__name__}"
    return " -> ".join(t.__name__ for t in s.__mro__[1:-1])


print(f"{'base order':<22s} {'order after the built class':<34s}")
for bases in ((Timed, Signed), (Signed, Timed),
              (Timed, Record), (Record, Timed)):
    name = "(" + ", ".join(t.__name__ for t in bases) + ")"
    print(f"{name:<22s} {try_building(bases):<34s}")

print()
print(f"Record's position in Document's order: "
      f"{Document.__mro__.index(Record)} (last of the four classes)")
print(f"Timed's direct base: "
      f"{', '.join(t.__name__ for t in Timed.__bases__)}")
print(f"class right after Timed in Document's order: "
      f"{Document.__mro__[Document.__mro__.index(Timed) + 1].__name__}")
```

```
base order             order after the built class       
(Timed, Signed)        Timed -> Signed -> Record         
(Signed, Timed)        Signed -> Timed -> Record         
(Timed, Record)        Timed -> Record                   
(Record, Timed)        !TypeError                        

Record's position in Document's order: 3 (last of the four classes)
Timed's direct base: Record
class right after Timed in Document's order: Signed
```

**Three** of four orderings build, **one** fails. The first two lines show base-writing order
directly decides the result: swap the places and the class answering `source` changes too.
The fourth line draws the boundary — with `Record` written first and its derivative `Timed`
second, the rule "every class comes before its bases" cannot be satisfied, and the class
cannot be built at all. This is not a runtime error; it is raised **at definition time**. The
order is computed once per class and stored on it, not rebuilt on every access. Lookup cost,
for this reason, depends not on the tree's shape but on how far into the order a name is
found.

The last three lines close the question of where the first table's order came from. `Record`
is `Timed`'s **direct** base; despite that, the class coming right after `Timed` in
`Document`'s order is not `Record`, it is **`Signed`**. The order does not follow a single
branch straight up the tree; once it passes a class it does not jump to that branch's base —
it **pushes the common base to the very end.**

The reason sits in the first measurement. If the order were
`Document -> Timed -> Record -> Signed`, `source` lookup would stop at `Record`, and the body
`Signed` writes would never be found — a method one sibling rewrites lost because of the
other sibling's base. Pushing the common base to the end prevents this: the base is only
asked once **all** its derivatives have been tried. The order's two rules are therefore not a
style choice, they are the condition for multiple inheritance to make sense at all.

#### `super()` Goes to the Order, Not the Ancestor

The most visible consequence of this is the `super()` call. Its name reads like an "ancestor
class" call, but that is not what it does: `super()` goes to the class **right after** the
one the body is written in, **in the calling object's resolution order**. The body stays the
same, the order changes, the destination changes.

- **CD23** — Four new classes are built for this measurement; the rig's tree is untouched.
- **CD24** — All four classes write the same name, and its body is a single line: put its own
  name on the list, and continue the chain via `super()`. The base class ends the chain.
- **CD25** — What is measured is not the call's result but the **sequence of bodies that
  ran**; the number is the body count.
- **CD26** — The second table reads `LeftLayer`'s body through two separate instances; the
  body is one, the instances are two different classes.

```python
class Layer:
    def label(self):
        return ["Layer"]


class LeftLayer(Layer):
    def label(self):
        return ["LeftLayer"] + super().label()


class RightLayer(Layer):
    def label(self):
        return ["RightLayer"] + super().label()


class Combined(LeftLayer, RightLayer):
    def label(self):
        return ["Combined"] + super().label()


def next_class(cls, body):
    """Where does the super() call written in class 'body' go?"""
    m = cls.__mro__
    return m[m.index(body) + 1].__name__


print(f"{'instance class':<14s} {'bodies run':>10s}  chain")
for s in (LeftLayer, Combined):
    chain = s().label()
    print(f"{s.__name__:<14s} {len(chain):>10d}  {' -> '.join(chain)}")

print()
print(f"{'body written in':<16s} {'instance class':<16s} super() goes to")
for s in (LeftLayer, Combined):
    print(f"{'LeftLayer':<16s} {s.__name__:<16s} {next_class(s, LeftLayer)}")
```

```
instance class bodies run  chain
LeftLayer               2  LeftLayer -> Layer
Combined                4  Combined -> LeftLayer -> RightLayer -> Layer

body written in  instance class   super() goes to
LeftLayer        LeftLayer        Layer
LeftLayer        Combined         RightLayer
```

`LeftLayer`'s body is **identical** in both measurements and a single line. On a `LeftLayer`
instance, the `super()` call in that line goes to `Layer`, and the chain ends at **2**
bodies. On a `Combined` instance, the same line goes to **`RightLayer`** — a class that is
not `LeftLayer`'s base and whose name does not even appear in its body — and the chain ends
at **4** bodies.

`RightLayer` never being skipped is the whole point of this arrangement: in multiple
inheritance, every class's body runs **exactly once**, and the common base runs **once** at
the very end. For the chain to work, every link has to call `super()`; once one class stops
calling it, every body after it silently drops out. Setup methods follow the same chain:
when `super().__init__(...)` is not written inside `__init__`, the intermediate classes'
setup bodies never run at all, and the gap only surfaces once a missing field is read.

The chain's second condition is signatures. Since a body does not know which class `super()`
will land on, every body sharing a name has to be written with parameters that **accept each
other**. Whoever wrote `LeftLayer`'s body may never have seen `RightLayer`; even so, the two
run side by side in the same call. Composition carries no such implicit contract —
`Wrapper.__init__` builds the two inner objects explicitly, by name and by argument, and the
destination is written in the body, not in an order.

The design-side conclusion follows from this. Reading a body correctly is not enough to know
the class it sits in; **which object the call came from** also has to be known. Composition
has no such unknown: the call inside `Wrapper.format`'s body goes to `self.signed`, and that
name is written inside the body itself. Inheritance gives a short write and hides who
answers; composition demands a longer write and writes who answers.

What is hidden here is not unreadable, though. Resolution order is not a guess, it is a list
sitting on the class and readable at any time; the `answering_class` function is three lines
walking that list. A capability's body does not have to be traced by eye through the class
tree — the order can be asked directly. Inheritance's cost is not that information
**disappears**, it is that reading the call requires asking for it separately.

#### Summary

- Which class answers a capability is decided by the **method resolution order**: if a name
  is written in more than one class, whichever comes first in the order wins, and the others
  are never called.
- `Document` carries **0** of the four capabilities in its own body, and the answer comes
  from three separate classes; `Wrapper` writes all **4** of the same four capabilities in
  its own body.
- `format`'s body is written in `Record`, but since the call inside it resolves through
  `self`, the result comes out **`<Signed>`**: reading a body does not tell you its result.
- Resolution order is built by two rules — every class before its bases, bases in the order
  they are written. **Three** of four base orderings build; the **one** that violates the
  rule falls with a `TypeError` at definition time.
- `super()` does not go to the base of the class the body is written in — it goes to
  **whatever comes after that class in the instance's order**: the same `LeftLayer` body goes
  to `Layer` in one instance and to `RightLayer` in another, and the chain runs **2** versus
  **4** bodies.

#### Next Step

Up to here, every name was assumed visible from outside: `Document`'s instance `source` and
the `signed` object `Wrapper` holds inside itself could both be read the same way. But
`signed` is `Wrapper`'s internal arrangement; code touching it from outside binds itself to a
detail `Wrapper` might change tomorrow. This is another place the two designs part ways: in
inheritance every base class's name passes to the derivative as is; in composition, what is
exposed outward is written name by name in the body. The next lesson measures how Python
declares this distinction: does an underscore in front of a name actually **block** access,
what does a double underscore change, and if it does not block access, does the declaration
have any measurable effect at all?

### Encapsulation Conventions

Source: https://academia.sh/en/courses/python-object-and-types/encapsulation-conventions

3 of four access forms give a value, and the access the name blocks is 0; a double underscore is not a lock but a name change, and it opens 2 entries instead of 1 for two classes' same name.

The previous lesson compared two designs through the class that answers, and showed that
`Wrapper` holds two objects inside itself. Those two objects are `Wrapper`'s **internal
arrangement**: tomorrow one might be dropped, renamed, or the two merged into one. But
nothing in the measurement stopped anyone from touching them from outside —
`object.signed` is exactly as valid a spelling as `object.source()`.

This lesson measures how that boundary is declared. Some languages close visibility off with
the language itself; Python does not. The question is: does an underscore in front of a name
actually **block** access, what does a double underscore change, and if it does not block
access, does the declaration have any measurable effect at all?

#### Three Namings, Four Accesses

A class can use three separate naming forms. A name without an underscore is **open to the
outside**: its name, type, and meaning are part of the promise the class makes. A name
starting with one underscore is **internal**: the class uses it, an outside reader reads it
at their own responsibility, and the class can change it without notice. A name starting with
two underscores triggers the language's **name mangling** rule; the class's name is prefixed
onto it.

All three are assigned the same way and all three are written to the same dict. What
separates them is which spelling can read them from outside.

- **CD27** — Three names are set up in a single `__init__` body, in three assignments
  identical to each other; their values reflect their names too.
- **CD28** — Four access forms are tried: three names directly, and the double-underscore
  name also with its mangled spelling. A form that cannot access it records the **exception's
  name** with an exclamation prefix.
- **CD29** — A read from the class's own body is also printed; the measurement compares
  outside access against inside access.
- **CD30** — Names in the instance dict are printed sorted; **no identity number or
  environment-dependent data is printed.**

```python
"""What does the name actually block: three visibility declarations and four access forms."""


class Store:
    """Three separate naming forms; all three are assigned the same way."""

    def __init__(self):
        self.open = "open"
        self._inner = "inner"
        self.__closed = "closed"

    def self_read(self):
        return (self.open, self._inner, self.__closed)


d = Store()

FORMS = (
    ("d.open", lambda: d.open),
    ("d._inner", lambda: d._inner),
    ("d.__closed", lambda: getattr(d, "__closed")),
    ("d._Store__closed", lambda: d._Store__closed),
)


def attempt(call):
    try:
        return repr(call())
    except AttributeError as e:
        return f"!{type(e).__name__}"


print(f"{'form':<22s} {'access from outside':>19s}")
for name, call in FORMS:
    print(f"{name:<22s} {attempt(call):>19s}")

passing = sum(1 for _, c in FORMS if not attempt(c).startswith("!"))
print()
print(f"the class itself reads all three -> {d.self_read()}")
print(f"names in instance dict -> {sorted(d.__dict__)}")
print(f"{passing} of four access forms gave a value; "
      f"access blocked by the name {len(FORMS) - passing - 1}")
```

```
form                   access from outside
d.open                              'open'
d._inner                           'inner'
d.__closed                 !AttributeError
d._Store__closed                  'closed'

the class itself reads all three -> ('open', 'inner', 'closed')
names in instance dict -> ['_Store__closed', '_inner', 'open']
3 of four access forms gave a value; access blocked by the name 0
```

**Three** of four forms give a value. The first two lines produce the expected result: both
the open name and the single-underscore name read from outside. A single underscore has no
effect on access at all; as far as the language is concerned, there is **no difference** at
all between `_inner` and `open`. The only thing separating them is a declaration read by
humans and tools.

The third line looks like a block, but it is not; the fourth line shows this. The same value
reads without trouble under the spelling `d._Store__closed`. Looking at the instance dict
shows why: that name's actual entry in the dict is already `_Store__closed`. A double
underscore is not a lock, it is a name change made **at compile time** — the spelling
`self.__closed` written inside the class body is translated into that name prefixed with the
class's own name. `d.__closed` fails not because access was closed, it is because
**no such name exists at all.**

This is why the last line's number is **0**: access was not blocked on any of the four forms,
one form looked for the wrong name, nothing more. In Python, visibility is not a rule enforced by
the language, it is **a declaration the class makes**; whether to honor it is the caller's
decision, and the cost of not honoring it is paid not at runtime but when the class changes.

Since the declaration has a recipient, it is not an empty gesture either. Underscore-prefixed
names count as outside the class's open surface: a reader asking which names of a class are
safe to use, a tool generating help text, and the rule deciding which names
`from ... import *` brings in all read the same signal. The declaration's power comes not
from the language enforcing it, but from **everyone reading it the same way**. The previous
lesson's `Wrapper` class is an example: had the two objects it holds inside been named with a
`_`, the responsibility for breakage in code touching them from outside would have been
explicitly handed off.

#### What Name Mangling Resolves

If a double underscore does not block access, what is it for? It has a measurable job, and it
does not show up in a single class alone: it appears when two classes use the **same name**.
In inheritance, a base and a derivative share a single instance dict; if both write
`self.state`, the second write **erases** the first. Name mangling removes this collision by
prefixing every class's own name onto it.

- **CD31** — A base and a derivative write the same two names; the derivative's setup body
  calls the base's first, then assigns its own values.
- **CD32** — Each class's own read is printed separately; what is measured is **which value**
  the same spelling sees in the two bodies.
- **CD33** — The last line counts how many entries the names open in the instance dict.

```python
class Base:
    def __init__(self):
        self._state = "Base"
        self.__state = "Base"

    def base_read(self):
        return {"single": self._state, "double": self.__state}


class Derived(Base):
    def __init__(self):
        super().__init__()
        self._state = "Derived"
        self.__state = "Derived"

    def derived_read(self):
        return {"single": self._state, "double": self.__state}


t = Derived()
print(f"{'form':<14s} {'Base body reads':>16s} {'Derived body reads':>19s}")
for key, name in (("single", "_state"), ("double", "__state")):
    print(f"{name:<14s} {t.base_read()[key]:>16s} {t.derived_read()[key]:>19s}")

print()
print(f"names in instance dict -> {sorted(t.__dict__)}")
print(f"entries for the two classes' single-underscore name "
      f"{sum(1 for a in t.__dict__ if a == '_state')}, "
      f"for the double-underscore name {sum(1 for a in t.__dict__ if a.endswith('__state'))}")
```

```
form            Base body reads  Derived body reads
_state                  Derived             Derived
__state                    Base             Derived

names in instance dict -> ['_Base__state', '_Derived__state', '_state']
entries for the two classes' single-underscore name 1, for the double-underscore name 2
```

The first row is the collision itself. When `Base`'s body reads `self._state`, it sees
**`Derived`** — not the value it wrote itself. Since the derivative's setup ran after the
base's, it overwrote the same dict entry, and the base's value was lost. The base class
assumed it was the only side writing this name; a subclass broke it without knowing.

The second row has no collision. Each body reads the value it wrote itself, because the two
spellings were translated into two separate names. The numbers below confirm this: the
single-underscore name opens **1** entry for the two classes, the double-underscore name
opens **2**. The names in the dict show this too.

This is exactly, and only, what name mangling resolves: that a class's internal name stays
**its own**, no matter who derives from it. If a class is a base others are expected to
derive from and its internal state should not be overwritten by subclasses, a double
underscore gives a measurable guarantee. For an ordinary internal name, a single underscore
is enough — a double underscore there only lengthens the spelling and needlessly complicates
a subclass's access. It should be added that name mangling is tied to **the class's name**:
if two classes in two separate places happen to share a name, the prefix comes out the same
and the guarantee drops. The promise made is not "no one can access it," it is "classes not
sharing the same name do not collide."

#### A Declaration Does Not Protect an Invariant

Encapsulation's purpose is not hiding a name, it is holding up an **invariant**. Protecting
state and maintaining invariants in object-oriented programming were established in the
Programming Paradigms course; **that discussion is not repeated here.** What is measured here
is one question: is a naming declaration enough to protect an invariant?

- **CD34** — The measured class has one invariant: the held total must equal the sum of the
  items. Consistency is checked after every write.
- **CD35** — Five writes are tried: three through the class's method, two directly on the
  internal names. Both use single-underscore names, and **neither is blocked.**
- **CD36** — The "passed through body" column shows whether the write went through the
  class's method; the other column shows whether the invariant is standing after that write.
- **CD37** — Writes are made in order on the same object; an invariant broken stays broken in
  the following rows too.

```python
class Counter:
    """Invariant: _total always equals the sum of _items."""

    def __init__(self):
        self._items = []
        self._total = 0

    def add(self, n):
        self._items.append(n)
        self._total += n

    def consistent(self):
        return self._total == sum(self._items)


s = Counter()
WRITES = (
    ("s.add(2)", lambda: s.add(2)),
    ("s.add(3)", lambda: s.add(3)),
    ("s.add(5)", lambda: s.add(5)),
    ("s._total = 99", lambda: setattr(s, "_total", 99)),
    ("s._items.append(7)", lambda: s._items.append(7)),
)

print(f"{'write':<21s} {'passed through body':>19s} {'invariant held':>15s}")
passed = held = 0
for name, action in WRITES:
    through_body = name.startswith("s.add")
    action()
    ok = s.consistent()
    passed += through_body
    held += ok
    print(f"{name:<21s} {str(through_body):>19s} {str(ok):>15s}")

print()
print(f"writes {len(WRITES)}, passed through body {passed}, "
      f"blocked by the name 0, invariant held {held}")
```

```
write                 passed through body  invariant held
s.add(2)                             True            True
s.add(3)                             True            True
s.add(5)                             True            True
s._total = 99                       False           False
s._items.append(7)                  False           False

writes 5, passed through body 3, blocked by the name 0, invariant held 3
```

**Three** of five writes passed through the class's body, and the invariant stood in all
three. Two did not pass, and both broke the invariant. The number of writes blocked by the
name is still **0**: both violations were made on single-underscore names, and neither
raised an exception.

The last line also shows a boundary being broken. `s._items.append(7)` is not even an
**assignment**; it is a mutation performed on an object that was read. Handing out a name for
reading also means handing out the mutable object it points to. This side of encapsulation
can never be solved by naming alone; it is solved only by handing out a copy, or an immutable
container, and both are decisions made inside the body.

Running the measurement in order shows one more point. The invariant broken on the fourth row
stays broken on the fifth too; the object does not repair itself, and nobody questions its
state. Where the breakage will surface is not the line the violation was made on, it is
whatever line later **reads** the total. This gap between violation and symptom is the real
cost of unchecked writing.

The answer to "who" in this lesson is singular: **the only place protecting the invariant is
the `add` body.** Whatever the name does, every write that bypasses that body is
unsupervised. A naming declaration says **which spellings are supported**; it is not the side
that **enforces** the contract. Writing the enforcing side is a separate job, and it requires
catching the access itself by putting a body in the way.

#### The One Declaration That Actually Blocks

If naming blocks nothing, is there a declaration that does? There is one, and it declares not
visibility but the **attribute set**. An array written with the name `__slots__` in a class
body fixes which names that class's instances can carry; writing to an undeclared name falls
at runtime, and instances carry no separate dict.

- **CD38** — Two classes share the same setup body; the only difference is that one declares
  its attribute set and the other does not.
- **CD39** — Two writes are tried: to a declared name, and to a name misspelled by one letter.
  The second is deliberately wrong.
- **CD40** — The "instance dict" column reports whether the instance carries a separate
  attribute dict; **its content and size are not printed.**
- **CD41** — The "blocked" column counts how many of the two writes fell with an exception.

```python
class Free:
    """Does not declare an attribute set: any name can be written."""

    def __init__(self, label):
        self.label = label


class Restricted:
    """Declares an attribute set: only declared names can be written."""

    __slots__ = ("label",)

    def __init__(self, label):
        self.label = label


def write(obj, field):
    try:
        setattr(obj, field, "new")
    except AttributeError as e:
        return f"!{type(e).__name__}"
    return "passed"


print(f"{'class':<11s} {'label write':>15s} {'lable write':>15s} "
      f"{'instance dict':>14s} {'blocked':>8s}")
for cls in (Free, Restricted):
    n = cls("k")
    result = [write(n, "label"), write(n, "lable")]
    print(f"{cls.__name__:<11s} {result[0]:>15s} {result[1]:>15s} "
          f"{('yes' if hasattr(n, '__dict__') else 'no'):>14s} "
          f"{sum(1 for c in result if c.startswith('!')):>8d}")
```

```
class           label write     lable write  instance dict  blocked
Free                 passed          passed            yes        0
Restricted           passed !AttributeError             no        1
```

The first row shows the silence of the undeclared case: the misspelled name **passes** too,
and adds a new attribute to the object. The program raises no error; `label` just stays at
its old value and nobody notices. In the row below, the same write **falls**; blocked writes
rise to **1**.

This is the lesson's one real block, and notably it is not a **visibility** block. The
`__slots__` declaration says which names can be written; it says nothing, again, about which
names can be read from outside. A declared name, even underscore-prefixed, stays open to
outside reads. So the only encapsulation rule Python actually enforces is the set's
**closedness**, not its **secrecy**.

The absence of an instance dict explains the mechanism behind this too. Once declared,
instance names are not held in a free-form dict; a fixed slot is set up on the class for each
name, and there is nowhere to write outside those slots. The source of the block is not a
prohibition, it is **the absence of a place.**

#### Summary

- Visibility in Python is not a rule enforced by the language, it is a declaration made
  through naming; **3** of four access forms give a value, and access blocked by the name is
  **0**.
- A single underscore has no effect on access at all; it declares that a name belongs to the
  class's internal arrangement and can change without notice.
- A double underscore is not a lock, it is a **name mangling** that prefixes the class's own
  name; `d.__closed` falls not because access is closed, but because that name never existed.
- Name mangling's measurable job is preventing collisions: a base's and a derivative's same
  name opens **1** entry under a single underscore and the base's value is lost; under a
  double underscore it opens **2** entries and each body reads its own value.
- The only place protecting an invariant is the method body; **3** of five writes passed
  through the body and the invariant stood in all three, the **2** that did not break it.
- The only declaration actually enforced is `__slots__`, which closes the attribute set: a
  misspelled name **passes** silently on an undeclared class, and **falls** on a declared
  one. What is closed is the set, not visibility.

#### Next Step

Up to here, the classes we wrote were called through their own method names: `add`, `format`,
`source`. But most of the protocols measured in the previous two courses were called not by
method name but through **syntax** — `len(n)`, `n == m`, `with n`, `for x in n`. Those syntax
forms were answered by objects the language provided, and the answering side was always
ready. The next lesson measures that same situation with a class we write ourselves: how many
of these syntax forms can our own class join without writing any special method, what happens
where it cannot, and which syntax form does each method we write open?

### Special Methods

Source: https://academia.sh/en/courses/python-object-and-types/special-methods

A class writing no special method joins 5 of eleven syntax forms but never answers any of them itself; writing `__eq__` empties out the `__hash__` name, and a single method can close two syntax forms at once.

The previous three lessons called classes through methods we wrote ourselves: `add`,
`format`, `source`. But the Python Fundamentals course measured that syntax is a shorthand —
`n + m` is a method call, `x in n` is a method call, `for` is a protocol. **Those lists are
not repeated here.** What was measured there was the **calling** side, and the answering side
was always ready: a list, a string, a dict, a generator. The objects joining that protocol
were given by the language.

This lesson measures the case where we write the answering side ourselves. There are two
questions. First: how many of these syntax forms can a class join without writing a single
special method, and when it does join, **who gives the answer**? Second: does every method we
write open a syntax form — and does it ever close one too?

#### What an Unwritten Class Can Do

Eleven syntax forms are chosen; each looks for a specific special method name. Two classes
carry the same data. `Bare` writes no special method at all. `Written` writes all eleven of
the eleven methods. The measurement asks two things separately: did the syntax **work**, and
which class **provides** that name. The two questions have to be asked separately, because
their answers do not line up on every row — a syntax can fail even with the name found, and
can work even without it being found.

- **CD42** — Two classes carry the same three items, and their `__init__` bodies are
  identical; the only difference between them is which special methods are written.
  `Written` does not derive from `Bare`.
- **CD43** — Each form is run on two fresh instances; the measurement looks not at the
  result's value but at whether the call was answered. An unanswered call falls with
  `TypeError`.
- **CD44** — The "answering" column gives the first class carrying the name during
  resolution; if no class carries it, `None` is printed.
- **CD45** — No syntax's **result is printed**. Since `Bare` writes no representation method,
  the default representation carries **environment-dependent data**; the measurement therefore
  prints only the status and the answering class.

```python
"""We write the side that joins the syntax: an unwritten class and a written class."""


def answering_class(cls, name):
    for s in cls.__mro__:
        if name in s.__dict__:
            return s.__name__
    return None


class Bare:
    """Writes no special method at all; only carries its items."""

    def __init__(self, items=(1, 2, 3)):
        self.items = list(items)


class Written:
    """Carries the same data, writes one method for every syntax it joins."""

    def __init__(self, items=(1, 2, 3)):
        self.items = list(items)

    def __repr__(self):
        return f"Written({self.items})"

    def __str__(self):
        return "-".join(str(x) for x in self.items)

    def __len__(self):
        return len(self.items)

    def __eq__(self, o):
        return isinstance(o, Written) and self.items == o.items

    def __lt__(self, o):
        return len(self.items) < len(o.items)

    def __bool__(self):
        return bool(self.items)

    def __contains__(self, x):
        return x in self.items

    def __iter__(self):
        return iter(self.items)

    def __enter__(self):
        return self

    def __exit__(self, *k):
        return False

    def __getitem__(self, i):
        return self.items[i]

    def __hash__(self):
        return hash(tuple(self.items))


def use_context(n):
    with n:
        return True


FORMS = (
    ("repr(n)", "__repr__", lambda n, m: repr(n)),
    ("str(n)", "__str__", lambda n, m: str(n)),
    ("len(n)", "__len__", lambda n, m: len(n)),
    ("n == m", "__eq__", lambda n, m: n == m),
    ("n < m", "__lt__", lambda n, m: n < m),
    ("bool(n)", "__bool__", lambda n, m: bool(n)),
    ("2 in n", "__contains__", lambda n, m: 2 in n),
    ("for x in n", "__iter__", lambda n, m: [x for x in n]),
    ("with n", "__enter__", lambda n, m: use_context(n)),
    ("n[0]", "__getitem__", lambda n, m: n[0]),
    ("hash(n)", "__hash__", lambda n, m: hash(n)),
)


def measure(cls, action):
    try:
        action(cls(), cls())
    except TypeError:
        return "fell"
    return "worked"


print(f"{'syntax':<12s} {'special method':<15s} {'Bare':>7s} "
      f"{'answering':>11s} {'Written':>9s} {'answering':>11s}")
for name, method, action in FORMS:
    print(f"{name:<12s} {method:<15s} {measure(Bare, action):>7s} "
          f"{str(answering_class(Bare, method)):>11s} {measure(Written, action):>9s} "
          f"{str(answering_class(Written, method)):>11s}")

for cls in (Bare, Written):
    worked = sum(1 for _, _, f in FORMS if measure(cls, f) == "worked")
    own = sum(1 for _, m, _ in FORMS if answering_class(cls, m) == cls.__name__)
    print(f"\n{cls.__name__}: worked {worked}/{len(FORMS)}, "
          f"answered by itself {own}/{len(FORMS)}")
```

```
syntax       special method     Bare   answering   Written   answering
repr(n)      __repr__         worked      object    worked     Written
str(n)       __str__          worked      object    worked     Written
len(n)       __len__            fell        None    worked     Written
n == m       __eq__           worked      object    worked     Written
n < m        __lt__             fell      object    worked     Written
bool(n)      __bool__         worked        None    worked     Written
2 in n       __contains__       fell        None    worked     Written
for x in n   __iter__           fell        None    worked     Written
with n       __enter__          fell        None    worked     Written
n[0]         __getitem__        fell        None    worked     Written
hash(n)      __hash__         worked      object    worked     Written

Bare: worked 5/11, answered by itself 0/11

Written: worked 11/11, answered by itself 11/11
```

`Bare` joins **5** of eleven forms, and the number of forms it answers itself is **0**. Not
writing a method does not close the syntax; **it hands it off to a silent default.** In four
of the five working rows the answer comes from the common base of every class; on the
`bool(n)` row no class gives it at all — there, instead of falling when it cannot find a
method, the syntax switches to the rule "every object is true."

What the defaults say matters, because all of them rest on **identity**. The equality the
common base gives does not count two separate objects equal; the hash it gives is also
produced from identity. If a `Bare` object needs to be compared by its items, or used as a
key by its items, these defaults give the **wrong** answer — they do not raise an error, they
give a **wrong answer**. This is the most expensive form of a bug: two `Bare` objects
carrying the same content, put in a set, count as two separate items, and the program keeps
running without a single warning.

The `n < m` row is a separate reading. The answering column is not empty, it shows the common
base; the name was found. Even so, the syntax **fell**. The common base defines this name, but
its body says "this job is unknown to me," and the comparison goes unanswered. **A name being
found does not mean an answer was given.** The correct way to look for a capability is not
whether the name exists, it is whether the call gets answered.

`Written` joins all eleven of eleven forms and answers all eleven itself. A class "resembling
the language's objects" is not a separate capability; **it is the methods written.**
Joining comes not from the type's name or its base, but from the names placed in the class's
dict.

One detail of the number does not show in the table. `Written` writes **twelve** bodies for
eleven syntax forms: the context syntax is a single line, but its entry and exit are answered
by two separate methods, and the table only lists the entry. The mapping between syntax and
method is not one to one; a syntax can ask for more than one body, or none at all, as seen on
the `bool(n)` row.

Some of the written methods also open syntax forms adjacent to themselves. A class writing
equality does not additionally write inequality; a class writing less-than also answers a
greater-than comparison between two objects of the same type, because the language flips the
operation and asks the other operand. These were measured from the **calling** side in the
Python Fundamentals course; here the same drops appear from the **writing** side — writing
one body answers every spelling that falls to that body at once.

#### Writing a Method Can Close a Syntax Form

Written methods do not just open a door. There is a contract the language upholds between
equality and hash value: two objects counted equal must also have equal hash values. When a
class defines equality itself, this contract can no longer be guaranteed automatically, and
the language does not leave the decision to the class — it **removes** the hashing
capability.

- **CD46** — Three classes carry the same single field; they differ only in which method they
  write. The third class derives from the second and only adds the hashing method.
- **CD47** — The hash test does not print the value itself, only whether the call was
  answered; **a hash value can be environment-dependent.**
- **CD48** — The last two columns separately show whether the `__hash__` name is found in the
  class's own dict, and whether its value is empty.

```python
class Plain:
    """Writes neither equality nor hash."""

    def __init__(self, value=1):
        self.value = value


class Equatable:
    """Writes only equality."""

    def __init__(self, value=1):
        self.value = value

    def __eq__(self, o):
        return isinstance(o, Equatable) and self.value == o.value


class EquatableHashable(Equatable):
    """Inherits equality, writes hashing itself."""

    def __hash__(self):
        return hash(self.value)


def attempt(action):
    try:
        return str(action())
    except TypeError as e:
        return f"!{type(e).__name__}"


print(f"{'class':<19s} {'a == b':>8s} {'hash(a)':>16s} {'key use':>16s} "
      f"{'__hash__ written':>17s} {'value empty':>12s}")
for cls in (Plain, Equatable, EquatableHashable):
    a, b = cls(), cls()
    print(f"{cls.__name__:<19s} {attempt(lambda: a == b):>8s} "
          f"{attempt(lambda: hash(a) is not None):>16s} "
          f"{attempt(lambda: len({a: 1})):>16s} "
          f"{str('__hash__' in cls.__dict__):>17s} "
          f"{str(cls.__hash__ is None):>12s}")
```

```
class                 a == b          hash(a)          key use  __hash__ written  value empty
Plain                  False             True                1             False        False
Equatable               True       !TypeError       !TypeError              True         True
EquatableHashable       True             True                1              True        False
```

The first row shows the default state: two separate objects are not equal, but a hash value
exists and the object can be used as a key in a container. In the second row, a single method
was written, and **two** syntax forms closed at once — both `hash(a)` and use as a key fell.
The method written was `__eq__`; both of the closing syntax forms were tied to `__hash__`.

The last two columns say how. The name `__hash__` **is written** into `Equatable`'s own dict,
and its value **is empty**. The language silently adds an entry to a class that writes
equality and leaves that entry empty; lookup can no longer reach the common base, it hits an
empty value at the first step. This is not a side effect, it is a deliberate decision: an
object whose equality has changed using its old hash value would turn it into a **lost** key
inside a container.

The third row shows the fix. `EquatableHashable` inherits equality and writes hashing itself;
the entry is no longer empty, and both syntax forms reopen. The rule is: **a class writing
equality also writes hashing** — or, by not writing it, accepts that the object cannot be a
key. When hashing is written, it has to use the same fields used in equality, or the contract
breaks again.

The second half of the contract is also the writing side's responsibility: a hash value must
not change over the object's **lifetime**. A class producing equality from a mutable field
makes the object unfindable in its own container if that field changes while it is a key.
This is why classes meant to be hashable are expected to produce equality from
**unchanging** fields. It is also why `Written`'s hash body in the first measurement turns its
items into a tuple: a mutable container cannot be hashed on its own.

#### Two Representations

The remaining distinction is between two representation methods. Both produce a string from
an object, but their targets differ: one introduces the object for the **interpreter**, the
other presents it to the **reader**. Which one gets called is decided by where it is used.

- **CD49** — Two classes return the same two strings; they differ only in that one does not
  write the text method.
- **CD50** — Six usage forms are tried, each run on a fresh instance; the measurement looks at
  **which** string comes back.
- **CD51** — The last lines count how many of the six forms go to the text method; printing
  inside a container is counted too.

```python
class ReprOnly:
    """Writes only __repr__."""

    def __repr__(self):
        return "repr"


class BothReprs:
    """Writes both."""

    def __repr__(self):
        return "repr"

    def __str__(self):
        return "text"


USAGE = (
    ("str(n)", lambda n: str(n)),
    ("repr(n)", lambda n: repr(n)),
    ("n in f-string", lambda n: f"{n}"),
    ("n!r in f-string", lambda n: f"{n!r}"),
    ("printing [n]", lambda n: str([n])),
    ("format()", lambda n: "{}".format(n)),
)

print(f"{'form':<17s} {'only __repr__ written':>22s} {'both written':>14s}")
for name, action in USAGE:
    print(f"{name:<17s} {action(ReprOnly()):>22s} {action(BothReprs()):>14s}")

for cls in (ReprOnly, BothReprs):
    text = sum(1 for _, f in USAGE if "text" in f(cls()))
    print(f"\n{cls.__name__}: form giving the __str__ result {text}/{len(USAGE)}")
```

```
form               only __repr__ written   both written
str(n)                              repr           text
repr(n)                             repr           repr
n in f-string                       repr           text
n!r in f-string                     repr           repr
printing [n]                      [repr]         [repr]
format()                            repr           text

ReprOnly: form giving the __str__ result 0/6

BothReprs: form giving the __str__ result 3/6
```

In the left column, all six forms give the same result, and the forms reaching the text
method are **0**. When the text method is not written, the request does not go unanswered,
**it falls back to the representation method.** The fall runs in one direction only: when the
representation method is not written, there is **no** falling back to the text method. If
only one of the two is going to be written, it should be the **representation**.

In the right column, **3** of six forms go to the text method. The split is clear: conversion
to a string and formatting present to the reader; the explicit call and the conversion marker
introduce to the interpreter. The fifth row is the most surprising — while an object sits
inside a container, it is **always** printed with the representation method, even if the
container itself has been converted to text from outside. When a container produces its own
text, it asks every item inside it for its representation, because what the interpreter will
see is not the item's presentation, it is its identity.

A writing rule follows from this: the representation method should carry enough information
to **rebuild** the object; the text method should be readable. `Written`'s two bodies in the
first measurement follow exactly this split: the representation body gives the class's name
together with its items, the text body lays out only the items in a readable form.

This distinction's counterpart in debugging is a directly measurable convenience. When a
hundred objects sitting in a container are dumped, what is shown is that class's
representation body; if representation is not written, the dump says nothing across a hundred
lines. A single written body earns readability across **every** container the object appears
in.

#### Summary

- A class writing no special method joins **5** of eleven syntax forms and answers **0** of
  them itself; not writing does not close the syntax, it hands it off to a silent default, and
  every default rests on identity.
- A name being found does not mean an answer is given: `n < m` finds a name on the common
  base, but the syntax still falls.
- A class writing all eleven methods joins all eleven syntax forms and answers all of them
  itself; joining comes from the methods written, not the type's name.
- Writing one method can close a syntax form: the class writing `__eq__` gets an **empty**
  `__hash__` name added to its dict, and both hashing and key use fall together. A class
  writing equality also writes hashing.
- When the text method is not written, the request falls back to the representation method,
  never the reverse; in a class writing both, **3** of six forms go to the text method, and
  printing inside a container always uses the representation.

#### Next Step

In this lesson, every written method was triggered by a **call**: writing `len(n)` ran a
body, writing `n[0]` ran a body. Reading an attribute, on the other hand, ran no body at all —
`n.items` was a dict lookup and nothing stepped in between. This is exactly the gap the third
lesson left open: a write that bypasses the body protecting the invariant stays unsupervised.
The next lesson measures catching access itself by putting a body in the way: for the same
number of reads and writes, how many bodies does a plain field run, how many does an
intercepting definition run, and where does the intervening code hold its data before three
instances start sharing a single value?

### Properties and Descriptors

Source: https://academia.sh/en/courses/python-object-and-types/properties-and-descriptors

The same seven accesses run 0 bodies on a plain field and let an invalid value through; an intervening definition runs 7 bodies and drops it. When a descriptor holds data on itself, three instances end up sharing one value.

The previous lesson wrote the side that joins syntax: `len(n)` ran a body, `n[0]` ran a body.
Reading an attribute, though, ran no body at all — `n.items` was a dict lookup and nothing
stepped in between. This is exactly the gap the third lesson left open: a write that bypasses
the body protecting the invariant stays unsupervised, and naming did nothing to stop it.

This lesson measures catching access itself. The measurement's axis does not change: what is
asked is still who answers the call, but this time the call does not look like a method
call — it looks like a field read. A read and a write to an attribute can also be routed
through a body; routing it does not change the spelling, it changes the cost. Three
questions are asked: how many **bodies** does the same access sequence run across two
designs, **where does** the intervening code **hold its data**, and of the two hooks
catching access, which one runs **how many times**?

#### How Many Bodies Did Access Run Through

Three classes offer the same single field, and all three are used from outside with the
spelling `n.value`. `Direct` keeps the field bare. `WithProperty` defines the field as a
**property**: it writes one body for reading and one for writing. `WithDescriptor` hands the
same check off to a **descriptor** — an object carrying the access logic in its own class,
built once in the class body.

All three classes are given the same job: increment the value three times, then try writing
an invalid value.

- **CD52** — All three classes offer their field under the name `value`; the calling side
  writes the same lines on all three.
- **CD53** — The measured work is seven accesses: three increments each produce **one read
  and one write**, and the last line adds one more write. Records made during setup do not
  count.
- **CD54** — The invalid value is deliberately negative; the check sits inside the written
  body and raises `ValueError`. Without the check, the value is written as given.
- **CD55** — The "final value" column is the value read from the field after the measurement
  ends; this read is not counted.

```python
"""How many times does access run through intervening code: a plain field, a property, a descriptor."""

LOG = []


class Direct:
    """The field is bare; access runs through no body at all."""

    def __init__(self):
        self.value = 0


class WithProperty:
    """Access runs through two intervening bodies."""

    def __init__(self):
        self._value = 0

    @property
    def value(self):
        LOG.append("read")
        return self._value

    @value.setter
    def value(self, new_value):
        LOG.append("write")
        if new_value < 0:
            raise ValueError("value cannot be negative")
        self._value = new_value


class Field:
    """Descriptor: carries the access logic in its own class."""

    def __set_name__(self, owner, name):
        self.name = "_" + name

    def __get__(self, obj, owner=None):
        LOG.append("read")
        return getattr(obj, self.name)

    def __set__(self, obj, new_value):
        LOG.append("write")
        if new_value < 0:
            raise ValueError("value cannot be negative")
        setattr(obj, self.name, new_value)


class WithDescriptor:
    """Hands the same check off to a descriptor."""

    value = Field()

    def __init__(self):
        self.value = 0


def measure(cls):
    LOG.clear()
    n = cls()
    LOG.clear()
    for _ in range(3):
        n.value = n.value + 1
    try:
        n.value = -1
    except ValueError as e:
        error = type(e).__name__
    else:
        error = "none"
    return list(LOG), n.value, error


print(f"{'class':<14s} {'reads':>6s} {'writes':>6s} {'total bodies':>13s} "
      f"{'final value':>11s} {'negative write':>14s}")
for cls in (Direct, WithProperty, WithDescriptor):
    log, final, error = measure(cls)
    print(f"{cls.__name__:<14s} {log.count('read'):>6d} "
          f"{log.count('write'):>6d} {len(log):>13d} {final:>11d} {error:>14s}")
```

```
class           reads writes  total bodies final value negative write
Direct              0      0             0          -1           none
WithProperty        3      4             7           3     ValueError
WithDescriptor      3      4             7           3     ValueError
```

Same lines, three different costs. `Direct` runs **0** bodies across seven accesses; access
goes straight to the instance's dict and back. `WithProperty` and `WithDescriptor` run **7**
bodies on the same seven accesses — three reads, four writes. The number matches the access
count exactly, because the intervening code runs on **every** access, not the first or some
of them.

The cost's payoff is in the last two columns. On the unchecked field, the negative write
**passed** and the final value became **-1**; the class was left in an invalid state and no
one knows it. On the two classes with an intervening body, the same write **fell** with
`ValueError` and the final value stayed at **3**, its last valid value. This is exactly where
the third lesson's gap closes: the only place protecting an invariant is a body, and the way
to route `n.value = -1` through that body is to put the field behind a definition.

The most important property of this design is that **nothing changes on the calling side.**
All three classes are used with the spelling `n.value`; whether the field is bare or answered
by two bodies is invisible from the calling side. Putting a field under a check later can be
done without changing a single line that uses it. This is why hiding fields behind
get–set methods up front is unnecessary: **a bare field is a field a body can be put behind
whenever needed.**

If two definitions give the same result, what decides between them? A property keeps its
bodies **inside the class itself**; it is the direct spelling for a check specific to one
class. A descriptor moves the logic to a **separate class**, and that class can be reused in
many places; if the same check is needed on ten fields, a property asks for ten pairs of
bodies, a descriptor asks for ten lines.

The same arrangement has two more common uses, and both pay the same measured number. First
is a **read-only** field: only a read body is written, no write body is written at all, and
assignment to the field falls with `AttributeError`. Fields whose value is set at
construction and should never change afterward are declared this way. Second is a
**computed** field: instead of returning a stored value, the read body produces it fresh
every time. Both look like an ordinary field from outside, and this is exactly where the
appearance is misleading — `n.value` reads like a free dict lookup, but a running body sits
behind it. Reading the same field a thousand times inside a loop means running a body a
thousand times. This is why the intervening body is expected to stay **cheap**; an expensive
operation is not presented as a field, it is presented as an explicit method call.

#### Where a Descriptor Holds Its Data

A descriptor has a trap, and it explains why the first measurement's definition wrote to the
instance's own dict. A descriptor object is built **in the class body**; that means there is
**one** per class, and every instance shares it. A descriptor holding its data on itself ends
up sharing that data too.

- **CD56** — Two descriptors do the same job; the only difference is where they write the
  data.
- **CD57** — Three instances are built and each writes its own index; then all three are
  read.
- **CD58** — The last column is the total entry count across the three instances' **own**
  dicts; their contents are not printed.

```python
class HeldOnSelf:
    """Holds the data on the descriptor object itself."""

    def __get__(self, obj, owner=None):
        return self.value

    def __set__(self, obj, new_value):
        self.value = new_value


class HeldOnInstance:
    """Holds the data in the accessed instance's dict."""

    def __set_name__(self, owner, name):
        self.name = "_" + name

    def __get__(self, obj, owner=None):
        return getattr(obj, self.name)

    def __set__(self, obj, new_value):
        setattr(obj, self.name, new_value)


class BadBox:
    value = HeldOnSelf()


class GoodBox:
    value = HeldOnInstance()


print(f"{'class':<10s} {'what three see':>16s} {'single descriptor':>18s} "
      f"{'in instance dict':>17s}")
for cls in (BadBox, GoodBox):
    trio = [cls() for _ in range(3)]
    for i, n in enumerate(trio):
        n.value = i
    definition = cls.__dict__["value"]
    print(f"{cls.__name__:<10s} {str([n.value for n in trio]):>16s} "
          f"{str(all(type(n).__dict__['value'] is definition for n in trio)):>18s} "
          f"{sum(len(n.__dict__) for n in trio):>17d}")
```

```
class        what three see  single descriptor  in instance dict
BadBox            [2, 2, 2]               True                 0
GoodBox           [0, 1, 2]               True                 3
```

Three instances wrote three separate values. On `BadBox`, all three read back the **last
value written**; the first two writes were lost, and the instances' own dicts carry **0**
entries. On `GoodBox`, each instance reads back what it wrote itself, and **3** entries were
opened. The middle column gives the reason: on both classes, all three instances share a
**single** descriptor object. When a descriptor writes its data onto itself, all three
instances' writes go to the same field on that same single object.

This is the same arrangement as the first lesson's container-in-the-class-body measurement,
turning up in another guise: **anything built in the class body is shared across
instances.** A descriptor, too, is built in the class body. This is why it takes the accessed
instance as its first argument — the place it will store its data is that instance, not
itself.

Where the name used to write to the instance comes from is a separate question too. A
descriptor is bound in the class body with `value = ...`, but the object itself does not
know that name; there is a hook called while the class is being built that lets it learn the
name given to it. Both the first measurement's definition and `HeldOnInstance` here write
that hook, and produce the name they will use on the instance from it. Without writing the
hook, the name would have to be given by hand, and if the same descriptor were used on two
fields, both would write to the same place — another form of `BadBox`'s result.

The answer to "who" in this measurement has two layers. The class answering the call is the
**descriptor's class** — `BadBox`'s own body has no body at all for `value`. But which data
the answer looks at is decided not by the answering side, **it is decided by where the data
is written.** When the two come apart, the measurement comes apart with them.

#### Two Hooks, Two Frequencies

The last form of catching access is not for individual fields, it is for **all** attribute
access. There are two hooks, and they are easy to confuse: one runs on every access, the
other runs only when normal lookup fails.

- **CD59** — A single class writes both hooks; both only keep a log and do not disturb normal
  lookup.
- **CD60** — Four names are read: two exist in the instance dict, two exist nowhere. Records
  made during setup are cleared.
- **CD61** — What is measured is **how many times** each hook runs; returned values are not
  printed.

```python
TRACE = []


class Hooked:
    """Two hooks: one on every access, the other only when lookup fails."""

    def __init__(self):
        self.first = 1
        self.second = 2

    def __getattribute__(self, name):
        TRACE.append(("every access", name))
        return object.__getattribute__(self, name)

    def __getattr__(self, name):
        TRACE.append(("not found", name))
        return None


n = Hooked()
TRACE.clear()
READS = ("first", "second", "third", "fourth")
for name in READS:
    getattr(n, name)

print(f"{'name read':<12s} {'in instance':>12s} {'every-access hook':>18s} "
      f"{'not-found hook':>15s}")
for name in READS:
    print(f"{name:<12s} {str(name in ('first', 'second')):>12s} "
          f"{sum(1 for k, a in TRACE if k == 'every access' and a == name):>18d} "
          f"{sum(1 for k, a in TRACE if k == 'not found' and a == name):>15d}")

print()
print(f"reads {len(READS)}, every-access hook ran "
      f"{sum(1 for k, _ in TRACE if k == 'every access')} times, not-found hook "
      f"{sum(1 for k, _ in TRACE if k == 'not found')} times")
```

```
name read     in instance  every-access hook  not-found hook
first                True                  1               0
second               True                  1               0
third               False                  1               1
fourth              False                  1               1

reads 4, every-access hook ran 4 times, not-found hook 2 times
```

Across four reads, the first hook ran **4** times, the second **2**. The numbers say where
each hook sits: the first is **in front of** lookup and runs no matter what name is read; the
second is **behind** lookup and only kicks in when the search comes back empty.

The practical consequence of this split is cost and risk. Once a hook running on every access
is written, **all** of the class's attribute reads — including method access — pass through
that body; in this measurement four reads became four calls, but on a real class that number
is as large as everything the class does. Worse, using `self.` inside that body's own code
retriggers the hook and sets up an infinite chain; this is why the measured body calls the
common base's lookup directly.

The hook that runs only on names not found, by contrast, is cheap in this measurement: it
never appears on the path of names that exist. Answering names that do not exist, offering
access delegated to another object, or computing a missing field on demand is exactly what
this hook is for. The rule is: **a per-field definition to check what exists, a not-found
hook to answer what does not.**

Reading the three measurements together shows that attribute access is not a single dict
lookup. When a name is read, the class's resolution order is checked first; if a descriptor
that **also has a write body** is found there, its read body runs and the instance's dict is
never even asked. If no such definition exists, the instance's dict is checked. If it is not
there either, lookup falls through to ordinary class values and read-only definitions, and
finally the not-found hook kicks in. This order also explains why the first measurement's
check could not be skipped: even if an entry with the same name were opened on the instance,
a definition with a write body **takes priority** on read. The first lesson's "instance
first, then class" rule is the **middle** of this chain; what this lesson adds is the bodies
writable at the chain's front and back. Controlling access means placing a link in this
chain — and every link is paid on every access.

One asymmetry in the chain is worth noting too: the write side is not as rich as the read
side. A descriptor can catch writes too, but code writing directly to the instance's dict
runs through none of the class's bodies. The **0** block the third lesson measured still
holds after this lesson; the only thing that changes is that the customary spelling now runs
through a body.

#### Summary

- An intervening definition runs on every access: seven accesses run **0** bodies on a plain
  field, **7** on a property or descriptor — three reads, four writes.
- The cost buys a check: an invalid write **passes** on a bare field and the final value
  becomes **-1**; on a class with the two bodies it **falls** with `ValueError` and the final
  value stays at **3**.
- The calling side writes the same lines on all three classes; a bare field can therefore be
  put behind a body later without changing any of its callers.
- A descriptor is built once in the class body and shared by every instance: with a
  definition holding its data on itself, three instances read **[2, 2, 2]** and **0** entries
  open in their own dicts; with a definition holding it on the instance, **[0, 1, 2]** is read
  and **3** entries open.
- Two hooks run at two separate frequencies: across four reads, the every-access hook fires
  **4** times, the hook that runs only on names not found fires **2**.

#### Next Step

Every body written up to here was written by hand. The previous lesson asked for twelve
bodies for eleven syntax forms; this lesson added two more bodies for a single field's check.
But most of the classes we write repeat the same pattern: they carry a few fields, get
compared by their fields, and get printed by their fields. The next lesson measures what
replacing this repetition with a short definition produces: which special methods does the
short definition actually write into the class's dict, which ones are missing next to a
hand-written equivalent, and how many new names does each option added to the definition put
into the dict?

### Data Classes

Source: https://academia.sh/en/courses/python-object-and-types/data-classes

A short definition of four source lines writes the same 4 special methods a hand-written ten-line class writes; the ordering option raises it to 8, freezing to 6, both together to 10, and on the frozen class 3 of five operations fall.

The previous two lessons counted written bodies: twelve bodies for eleven syntax forms, two
more for a single field's check. But most of the classes we write repeat the same pattern.
They carry a few fields, fill those fields at construction, get compared by their fields, and
get printed by their fields. Writing this pattern by hand in every class both grows longer
and carries the risk of forgetting a field on every write.

The standard library offers a short definition form for this pattern. This lesson's question
is not convenience, it is measurement: which special methods does the short definition
actually write into the class's dict, which ones are missing next to a hand-written
equivalent, and how many new names does each option added to the definition put into the
dict?

#### What a Short Definition Writes

Five classes carry the same two fields: a string and an integer. `HandWritten` writes its
construction, representation, and equality bodies by hand. The remaining four are **data
class** definitions: fields are declared by name and annotation, no bodies are declared.
The four definitions differ from each other only in the options given — one is the default,
one asks for ordering, one is frozen, one is both.

The measurement looks for ten special method names in each class's **own** dict. Names
coming from ancestors do not count; what is asked is what the short definition **writes**
into that class.

- **CD62** — All five classes' fields are the same; their names, order, and annotations are
  the same too.
- **CD63** — The "source lines" row counts how many filled source lines each definition
  holds; blank lines do not count, the decorator line does.
- **CD64** — The ten sought names are looked for only in the class's own dict; `answering_class`
  is not used, because what is measured is whether the name is **written**, not where it
  comes from.
- **CD65** — The "hash value" row looks not at whether the name exists but at **whether its
  value is empty**; the emptying the previous lesson measured shows up here again.

```python
"""Which special methods does a short definition actually write into the class dict."""

import inspect
from dataclasses import dataclass


class HandWritten:
    def __init__(self, name, number):
        self.name = name
        self.number = number

    def __repr__(self):
        return f"HandWritten(name={self.name!r}, number={self.number!r})"

    def __eq__(self, o):
        if not isinstance(o, HandWritten):
            return NotImplemented
        return (self.name, self.number) == (o.name, o.number)


@dataclass
class Default:
    name: str
    number: int


@dataclass(order=True)
class Ordered:
    name: str
    number: int


@dataclass(frozen=True)
class Frozen:
    name: str
    number: int


@dataclass(frozen=True, order=True)
class FrozenOrdered:
    name: str
    number: int


CLASSES = (HandWritten, Default, Ordered, Frozen, FrozenOrdered)
SOUGHT = ("__init__", "__repr__", "__eq__", "__hash__", "__lt__", "__le__",
          "__gt__", "__ge__", "__setattr__", "__delattr__")


def source_line(cls):
    """How many filled source lines the definition holds."""
    return sum(1 for s in inspect.getsource(cls).splitlines() if s.strip())


print(f"{'special method':<16s}" + "".join(f"{s.__name__:>14s}" for s in CLASSES))
for name in SOUGHT:
    print(f"{name:<16s}"
          + "".join(f"{('written' if name in s.__dict__ else '.'):>14s}"
                    for s in CLASSES))

print(f"{'total written':<16s}"
      + "".join(f"{sum(1 for a in SOUGHT if a in s.__dict__):>14d}"
                for s in CLASSES))
print(f"{'source lines':<16s}" + "".join(f"{source_line(s):>14d}" for s in CLASSES))
print(f"{'hash value':<16s}"
      + "".join(f"{('none' if s.__hash__ is None else 'set'):>14s}"
                for s in CLASSES))
```

```
special method     HandWritten       Default       Ordered        Frozen FrozenOrdered
__init__               written       written       written       written       written
__repr__               written       written       written       written       written
__eq__                 written       written       written       written       written
__hash__               written       written       written       written       written
__lt__                       .             .       written             .       written
__le__                       .             .       written             .       written
__gt__                       .             .       written             .       written
__ge__                       .             .       written             .       written
__setattr__                  .             .             .       written       written
__delattr__                  .             .             .       written       written
total written                4             4             8             6            10
source lines                10             4             4             4             4
hash value                none          none          none           set           set
```

The first two rows are the comparison itself. The hand-written definition writes **4** names
across **10** source lines; the short definition writes the same **4** names in **4** lines.
The written names are identical — the short definition leaves nothing out and adds nothing
extra. What the measurement really says is that the short definition's gain is not just six
lines. In the hand-written class, every field name is written in **all three** of the three
bodies — construction, representation, and equality. Adding a new field means all three
bodies have to be updated; in the short definition, a field is declared **once**. An equality
body that silently compares too little because of a forgotten update is this pattern's most
expensive mistake, and it never raises an exception.

The fourth row shows the same mechanism the previous lesson measured, turning up again.
`__hash__` **is written** in all five classes' dicts, but the row below says its value is
**empty** in the first three. Every class writing equality loses hashing capability, and the
short definition is no exception to this rule. The hand-written class is in the same
situation, for the same reason. The result is data classes' most common surprise: an object
comparable by value cannot be put into a set or used as a dict key unless it is frozen.

The options show clearly what they add to the dict. The ordering option adds **4** comparison
methods and raises the total to **8**. The freezing option adds **2** methods catching the
write and delete paths, raises the total to **6**, and brings back the hash value. Given
together, the total comes to **10** — a four-line definition produces a ten-body class.

This table also defines what the short definition is: not a definition, a **write
operation.** Field declarations in the class body are read, and the corresponding bodies are
placed into the class's dict. Every name placed in the dict is identical to what could be
hand-written — this is why hand-writing any method on a class written with the short
definition also **overrides** it, since it is not written on top of a name already present in
the body.

What is not written also has to be counted. The measurement searched for ten names, and most
of the fourth lesson's eleven syntax forms are not on this list: length, iteration,
membership, index access, and context syntax do not come from the short definition. The short
definition writes a **record**, not a container. If a data class is meant to behave like a
container, the bodies needed for that are still written by hand.

How fields are found also leaves a point open. The short definition reads which names are
fields from the **annotations** in the class body — the type name in the spelling `name: str`
declares the field's existence. What that type name does at runtime is a separate question,
measured in this course's second topic; what can be said here is that the short definition
uses it only as a **marker**.

#### A Mutable Default

What the short definition **rejects** is measurable too, not just what it writes. The Python
Fundamentals course measured that a default value is evaluated **once**, when the function is
defined, and a mutable default is shared across calls; **that measurement is not repeated
here.** What is asked here is what the same spelling produces in two definition forms.

- **CD66** — Three definitions try setting up the same field with the same default; the first
  is the hand-written class, the others are the short definition.
- **CD67** — A definition that fails to build records the **exception's name** with an
  exclamation prefix, and that row's measurement columns are left blank.
- **CD68** — Three instances are built from every definition that builds, and each appends
  its own index to the container; sharing is shown with `is`.

```python
from dataclasses import field


class HandBox:
    """The default value is evaluated once; three instances share it."""

    def __init__(self, items=[]):
        self.items = items


def short_definition(default):
    """Try building the same default with the short definition."""
    try:
        @dataclass
        class Temp:
            items: list = default
    except ValueError as e:
        return f"!{type(e).__name__}", None
    return "built", Temp


DEFINITIONS = (
    ("hand-written, default []", "built", HandBox),
    ("short definition, default []", *short_definition([])),
    ("short definition, factory function", *short_definition(field(default_factory=list))),
)

print(f"{'definition':<36s} {'at definition time':>19s} {'what three see':>17s} "
      f"{'container shared':>17s}")
for name, status, cls in DEFINITIONS:
    if cls is None:
        print(f"{name:<36s} {status:>19s} {'—':>17s} {'—':>17s}")
        continue
    trio = [cls() for _ in range(3)]
    for i, n in enumerate(trio):
        n.items.append(i)
    print(f"{name:<36s} {status:>19s} {str([len(n.items) for n in trio]):>17s} "
          f"{str(trio[0].items is trio[2].items):>17s}")
```

```
definition                            at definition time    what three see  container shared
hand-written, default []                           built         [3, 3, 3]              True
short definition, default []                 !ValueError                 —                 —
short definition, factory function                 built         [1, 1, 1]             False
```

The first row gives the known result: the hand-written definition **builds**, three instances
share one container, and each sees **3** items. The error does not surface at runtime either;
the program silently runs the wrong way.

The second row is the short definition's difference. The same spelling falls with
`ValueError` **at definition time**; the class never even gets built, and the place the error
surfaces is the very line it was written on. The third row shows the accepted spelling: when
what is given is not a value but a **factory function** to be called for every instance, the
definition builds and three instances see **[1, 1, 1]** — the container is not shared.

This is the same arrangement as the first lesson's class-body container measurement, turning
up for the third time. What the short definition adds here is not a new rule, it is **moving
an existing rule earlier**: instead of leaving a known-wrong spelling to runtime, it rejects
it at definition time.

How narrow this rejection is also has to be written down. The short definition only
recognizes a handful of mutable types it knows about; when a mutable class we wrote ourselves
is given as a default, the definition builds, and the sharing gives the same result as the
first row. What is caught is not a general rule, it is a commonly made spelling. The rule
itself has not changed: **a default value is built once at definition time**, and any
container that should belong to the instance is declared with a factory function.

#### Freezing's Measure

The last option closes an object's writability. The justification for immutability and the
concept of a value object were established in the Software Design and Architecture
Principles curriculum; **that discussion is not repeated here.** What is measured here is how
many operations a single option closes, and what it opens in exchange.

- **CD69** — Two classes carry the same two fields; the only difference is whether they are
  frozen.
- **CD70** — Five operations are tried, and each runs on a **fresh** instance; the operations
  do not affect each other.
- **CD71** — The last line compares **two separate** objects carrying the same values;
  identity is shown with `is` and **no identity number is printed.**

```python
from dataclasses import replace

OPERATIONS = (
    ("write to field", lambda n: setattr(n, "number", 9)),
    ("delete field", lambda n: delattr(n, "number")),
    ("open new field", lambda n: setattr(n, "extra", 1)),
    ("new object in its place", lambda n: replace(n, number=9)),
    ("put into a set", lambda n: len({n})),
)


def attempt(cls, action):
    try:
        action(cls("k", 1))
    except Exception as e:
        return f"!{type(e).__name__}"
    return "passed"


print(f"{'operation':<25s} {'Default':>21s} {'Frozen':>21s}")
for name, action in OPERATIONS:
    print(f"{name:<25s} {attempt(Default, action):>21s} {attempt(Frozen, action):>21s}")

print()
for cls in (Default, Frozen):
    fell = sum(1 for _, f in OPERATIONS if attempt(cls, f).startswith("!"))
    print(f"{cls.__name__}: fell out of five {fell}, "
          f"passed {len(OPERATIONS) - fell}")

a, b = Frozen("k", 1), Frozen("k", 1)
print(f"two separate objects: a is b -> {a is b}, a == b -> {a == b}, "
      f"items in the set -> {len({a, b})}")
```

```
operation                               Default                Frozen
write to field                           passed  !FrozenInstanceError
delete field                             passed  !FrozenInstanceError
open new field                           passed  !FrozenInstanceError
new object in its place                  passed                passed
put into a set                       !TypeError                passed

Default: fell out of five 1, passed 4
Frozen: fell out of five 3, passed 2
two separate objects: a is b -> False, a == b -> True, items in the set -> 1
```

Freezing closes **3** operations and opens **1**. All three that close are writes: writing to
a field, deleting a field, and opening a new field. The third is especially notable — it
closes the same door as the third lesson's attribute-set declaration, but for a different
reason. The operation that opens is putting into a set: since a frozen object has a hash
value, it can be a key in a container; an ordinary data class cannot.

The fourth row passes on both classes and shows how freezing is meant to be used. An
immutable object is not mutated, **a new one is put in its place**; the function the standard
library provides produces a new object with the requested fields changed. Writing is blocked,
rebuilding is not.

The last row defines what a frozen data class is: `is` **False**, `==` **True**, **1** item
in the set. Two separate objects stand in for each other because they carry the same values.
Being defined by value rather than identity is the sum of these three results — and that is
exactly what this lesson's definition produces.

Freezing's limit also has to be written down, because the table does not show it. What is
closed is the path of **writing to a field**, not the object the field points to. If a frozen
object's field holds a list, appending an item to that list meets no resistance at all — the
third lesson's measurement's last row applies here exactly the same way. Immutability is one
layer deep; an object being truly unable to change depends on its fields also being
unchangeable objects. The hash value carries the same limit: if a frozen class carries a list
in a field, the put-into-a-set row falls with `TypeError`, because a hash is produced from
its fields.

#### Summary

- The short definition writes, across **4** source lines, the same **4** special methods a
  hand-written **10**-line class writes into the class's dict; the written names are
  identical.
- As with any class writing equality, the short definition writes the `__hash__` name and
  leaves its value **empty**; hashing capability only returns with freezing.
- Options add names to the dict: ordering adds **4** comparison methods and raises the total
  to **8**, freezing adds **2** methods and raises it to **6**, both together make it **10**.
- A mutable default **builds** in the hand-written definition, and three instances share one
  container and see **[3, 3, 3]**; in the short definition it falls with `ValueError` **at
  definition time**, and written with a factory function, **[1, 1, 1]** is seen.
- Freezing closes **3** of five operations and opens **1**: three write paths fall, putting
  into a set passes, and putting a new object in its place passes on both classes.
- For two separate frozen objects, `is` is **False**, `==` is **True**, and **1** item
  remains in the set.

#### Next Step

This lesson counted what a class writes by looking at its dict; the previous lessons also
always searched for the answer in the class tree. Both methods rest on the same assumption:
knowing what an object can do requires knowing **which class** it comes from. But the fourth
lesson had already shown the opposite — joining came not from the type's name, it came from
the methods written. `Wrapper` derives from nothing, yet answers all four of the four
capabilities. The next lesson turns this duality into a test: does the same object pass a
check that **searches for ancestry**, or one that **searches for methods**, and which of the
two — and what does an abstract base's declared contract enforce, and at what point, and what
does the method-searching test actually look at?

### Abstract Base Classes and Protocols

Source: https://academia.sh/en/courses/python-object-and-types/abstract-base-classes-and-protocols

1 of three candidates passes the ancestry test, 3 pass the method test, and 3 answer the call; in the two tests' blind spots, 3 of three candidates are accepted but only 1 answers the call.

The previous lesson counted what a class writes by looking at its dict; the ones before it
searched for the answer in the class tree. Both methods rest on the same assumption: knowing
what an object can do requires knowing **which class** it comes from. But the fourth lesson
showed the opposite — joining came not from the type's name, it came from the methods
written. The second lesson's `Wrapper` class is the concrete form of this duality: it derives
from nothing, and answers all four of the four capabilities.

This lesson turns the duality into a test. Whether a contract is satisfied can be asked in
two separate ways. A **nominal** test searches for ancestry: "did this object derive from
that contract?" A **structural** test searches for methods: "does this object carry these
names?" Which of the two does the same object pass, and does passing the test say it will
actually answer the call? For the question to be measurable, three separate things are asked
of every candidate: the two tests' results, and **the call itself**. Where the three do not
agree is this lesson's finding.

#### Two Tests, One Object

Two contracts are set up, and both ask for the same two capabilities: `source` and `format`.
The first is an **abstract base class**; a class satisfying it must derive from it. The
second is a **protocol**; the test only looks at whether the names exist.

Three candidates are tested. `Document` comes from an inheritance tree and inherits both
capabilities from three separate classes. `Wrapper` derives from nothing and writes both
capabilities in its own body. `Declarer` derives from the abstract base and writes both
bodies too.

- **CD72** — The rig's four-class tree and `Wrapper` are the shared definition's form; the
  contracts are set up separately and the tree is untouched.
- **CD73** — Both contracts ask for the same two names; the only difference between them is
  what the test looks at.
- **CD74** — Three things are measured separately for every candidate: whether the names are
  actually found, the two tests' results, and the result the call gives.
- **CD75** — The last column is the string `format()` returns; it is the same result the
  second lesson measured, printed again here.

```python
"""Nominal and structural contract: which test does the same object pass."""

from abc import ABC, abstractmethod
from typing import Protocol, runtime_checkable


class Record:
    def source(self):
        return "Record"

    def format(self):
        return f"<{self.source()}>"


class Timed(Record):
    def stamp(self):
        return "time"


class Signed(Record):
    def source(self):
        return "Signed"

    def signature(self):
        return "signature"


class Document(Timed, Signed):
    pass


class Wrapper:
    def __init__(self):
        self.timed = Timed()
        self.signed = Signed()

    def source(self):
        return self.signed.source()

    def stamp(self):
        return self.timed.stamp()

    def signature(self):
        return self.signed.signature()

    def format(self):
        return f"<{self.source()}>"


class Formattable(ABC):
    """Nominal contract: demands ancestry."""

    @abstractmethod
    def source(self):
        ...

    @abstractmethod
    def format(self):
        ...


@runtime_checkable
class FormattableProtocol(Protocol):
    """Structural contract: looks for method names."""

    def source(self): ...

    def format(self): ...


class Declarer(Formattable):
    """Declares the contract by ancestry and writes both bodies."""

    def source(self):
        return "Declarer"

    def format(self):
        return f"<{self.source()}>"


CANDIDATES = (("Document", Document()), ("Wrapper", Wrapper()), ("Declarer", Declarer()))

print(f"{'candidate':<10s} {'has both methods':>17s} {'ancestry test':>15s} "
      f"{'method test':>13s} {'format() result':>16s}")
for name, n in CANDIDATES:
    print(f"{name:<10s} "
          f"{str(all(hasattr(n, y) for y in ('source', 'format'))):>17s} "
          f"{str(isinstance(n, Formattable)):>15s} "
          f"{str(isinstance(n, FormattableProtocol)):>13s} {n.format():>16s}")

nominal = sum(1 for _, n in CANDIDATES if isinstance(n, Formattable))
structural = sum(1 for _, n in CANDIDATES if isinstance(n, FormattableProtocol))
print(f"\ncandidates {len(CANDIDATES)}, passing ancestry test {nominal}, "
      f"passing method test {structural}, answering the call {len(CANDIDATES)}")
```

```
candidate   has both methods   ancestry test   method test  format() result
Document                True           False          True         <Signed>
Wrapper                 True           False          True         <Signed>
Declarer                True            True          True       <Declarer>

candidates 3, passing ancestry test 1, passing method test 3, answering the call 3
```

All three of the three candidates answer the call. Passing the ancestry test is **1**,
passing the method test is **3**. The two objects in between — `Document` and `Wrapper` — do
everything the contract asks for and still fail the nominal test, because that test looks not
at the work done, but at the **declared ancestry**.

The two candidates fail for separate reasons, and this shows how narrow the test is.
`Wrapper` does not even know the contract exists. `Document` carries both bodies the
contract asks for through inheritance — as the second lesson measured, `format`'s body is in
`Record`, `source`'s in `Signed`. Both sit exactly where they should, both work, and the test
still gives `False`.

The structural test accepts both objects, because it asks a different question: are the
names the contract asks for present on this object? If they are, where they come from does
not matter — its own body, inheritance, or composition. The contract here is defined as a
**surface**, not an **ancestry**.

The two tests' cost is not the same either. The nominal test looks at a single ancestry
relationship; its result is known the moment the class is built and is not recomputed on
every test. The structural test has to search the object for every name the contract asks
for, and as the contract grows, the number of names searched grows with it. The contract in
this measurement asks for two names; a contract asking for ten names makes the same test do
ten searches. The split does not change direction here either: the nominal test is cheap and
narrow, the structural test is expensive and broad.

Choosing between an interface and an abstract class, narrowing interfaces to the client, and
preserving a contract across subtypes were established in the Software Design and
Architecture Principles curriculum; **those discussions are not repeated here.** The only
thing measured here is **which test the same object passes.**

The measurement does not force a choice either; it says under which condition each test is
right. If we write both sides of the contract ourselves, a nominal declaration pays off: the
declaring class states clearly what it must provide, and is stopped if it falls short. If
**others** will write the objects satisfying the contract, a nominal declaration becomes a
burden — demanding that every acceptable object derive from our definition leaves out objects
like `Document` and `Wrapper` that do the job completely. This is why most of the
standard library's container contracts carry a hook that searches for bodies: when a class
writes the required methods, it counts as a provider of the contract without setting up any
ancestry at all.

#### When the Contract Is Enforced

The nominal contract also has an enforcement side. A class deriving from an abstract base has
to write all the declared bodies. But at which point is this requirement checked — when the
class is defined, or when an instance is built?

- **CD76** — Both classes derive from the same abstract base; one writes both bodies, the
  other only one.
- **CD77** — The "at definition time" column is read from the block running: since both
  classes can be defined, its value is constant. The "building an instance" column is the
  call's result, and a falling call records the **exception's name** with an exclamation
  prefix.

```python
class Incomplete(Formattable):
    """Declares the contract but writes only one of the two bodies."""

    def source(self):
        return "Incomplete"


def attempt(action):
    try:
        action()
    except Exception as e:
        return f"!{type(e).__name__}"
    return "passed"


print(f"{'class':<10s} {'at definition time':>19s} {'building an instance':>21s} "
      f"{'unwritten method':>17s}")
for cls in (Declarer, Incomplete):
    print(f"{cls.__name__:<10s} {'built':>19s} {attempt(cls):>21s} "
          f"{str(sorted(cls.__abstractmethods__)):>17s}")
```

```
class       at definition time  building an instance  unwritten method
Declarer                 built                passed                []
Incomplete               built            !TypeError        ['format']
```

The incomplete class **can be defined**. The error is not raised at definition time, it is
raised when an attempt is made to build the first instance, and the last column gives the
missing name. The distinction matters in practice: the incomplete class can sit in a file, be
imported, and serve as a base for other classes. Nothing happens as long as it is never
instantiated itself — this is exactly why the abstract base itself cannot be instantiated
either. Placing the check at this point is the right choice: an incomplete class can still be
a useful definition, but an incomplete **object** cannot answer the call while claiming to
carry the contract. What is stopped is not the definition, it is the **claim**.

This is notable as the one contract that is actually enforced. The third lesson measured that
naming blocks **0** access; here, the declared contract is genuinely enforced, and an
incomplete implementation is stopped at runtime. But the enforcement point is narrow: only
the **presence of the body** is tested. Whether the written body does the right thing, or
returns the right value, is not part of this test at all.

Tying the check to instance construction also grants a convenience. The set of unwritten
methods is recomputed for every subclass; a class can stay as an intermediate layer and
deliberately leave some bodies unwritten, and a class at the end of the chain completes the
gaps. As long as the intermediate class is never instantiated, it meets no obstacle. The
contract can thus be split across more than one class — what is checked is not a single
class's own body, it is the **entirety** of the bodies gathered along the resolution order.

#### The Two Tests' Blind Spots

Objects can be built that both tests accept but that do not answer the call. The two blind
spots mirror each other: the nominal test reads ancestry and does not look at the body; the
structural test reads the name and does not look at the signature.

- **CD78** — The first candidate writes no body at all and is **registered by hand** with the
  contract; the registration changes only the ancestry information.
- **CD79** — The second candidate carries both names, but both methods demand **one extra
  argument**; the names are correct, the calling form is wrong.
- **CD80** — The third candidate is the previous measurement's correct class, added as a
  comparison baseline. The call column is made with no arguments on every candidate.

```python
class Empty:
    """Writes no body at all; is registered with the contract by hand."""


Formattable.register(Empty)


class WrongSignature:
    """Carries both names, but both demand one extra argument."""

    def source(self, extra):
        return f"WrongSignature-{extra}"

    def format(self, extra):
        return f"<{self.source(extra)}>"


BLIND_SPOTS = (("Empty", Empty()), ("WrongSignature", WrongSignature()),
               ("Declarer", Declarer()))

print(f"{'candidate':<14s} {'ancestry test':>15s} {'method test':>13s} "
      f"{'format() call':>15s}")
for name, n in BLIND_SPOTS:
    print(f"{name:<14s} {str(isinstance(n, Formattable)):>15s} "
          f"{str(isinstance(n, FormattableProtocol)):>13s} "
          f"{attempt(n.format) if hasattr(n, 'format') else '!AttributeError':>15s}")

accepted = sum(1 for _, n in BLIND_SPOTS
               if isinstance(n, Formattable) or isinstance(n, FormattableProtocol))
answering = sum(1 for _, n in BLIND_SPOTS
                if hasattr(n, "format") and attempt(n.format) == "passed")
print(f"\ncandidates {len(BLIND_SPOTS)}, passing at least one test {accepted}, "
      f"answering the call {answering}")
```

```
candidate        ancestry test   method test   format() call
Empty                     True         False !AttributeError
WrongSignature           False          True      !TypeError
Declarer                  True          True          passed

candidates 3, passing at least one test 3, answering the call 1
```

**3** of three candidates pass at least one test; answering the call is **1**. The first row
is the nominal test's blindness: `Empty` writes no body at all, is registered with the
contract by hand, and the test gives `True`. Registration changes only ancestry information;
it never asks about the bodies written. The call falls with `AttributeError` — an object that
passed the test does not even carry the name being searched for.

The second row is the structural test's blindness. `WrongSignature` carries both names, and
the test gives `True`; but both methods demand one extra argument, and a no-argument call
falls with `TypeError`. The structural test looks at **whether the name exists**; it does not
look at its parameters, its return value, or what its body does. The surface's name matches,
its shape does not.

The two rows together give this lesson's finding. The nominal test verifies a **declared
intent**; it does not verify that the intent was fulfilled. The structural test verifies
**existing names**; it does not verify that those names are callable. Neither says "this
object answers the call," and an object accepted by both can still be wrong.

This finding's measured counterpart is completed when set next to the first table. In the
first measurement, **3** of three candidates answered the call, and the nominal test accepted
only **1** of them — the test was too **narrow**. In the third measurement, both tests each
accepted one object that did not answer the call — the tests were too **wide**. The same
tool under-filters in one direction and over-filters in the other. The right contract is not
in the test itself, it is in choosing the test **knowing what it measures**.

Where the blind spots close is also known, and it differs for each. The nominal test's blind
spot is specific to hand-written registration; when ancestry is set up through inheritance,
the second measurement's check kicks in, and an instance of an incompletely implemented class
can never even be built. Hand registration is a declaration that bypasses this check and
leaves the responsibility with whoever registers it. The structural test's blind spot does
not close: a name search done at runtime can never see the signature under any condition.
The side that could see the signature is a side that reads the code **without running it.**

This split turns into a writing rule in practice. A test passing is not a guarantee the call
will be answered; the test is only a **pre-filter**. Testing an incoming object is not a
substitute for calling it — and since an object that has passed the test can still fall on
the call, who closes the gap between the test and the call has to be decided separately.

#### Summary

- The nominal test searches for ancestry, the structural test searches for method names; of
  the same three candidates, **1** passes the first, **3** pass the second, and **3** answer
  the call.
- `Document` and `Wrapper` both carry the two bodies the contract asks for — one through
  inheritance, the other through composition — and both fail the nominal test; the test looks
  not at the work done, but at the declared ancestry.
- The contract declared by an abstract base is enforced **not at definition time**, but when
  the first instance is built: an incomplete class can be defined, its instantiation falls
  with `TypeError`, and the unwritten body's name is reported.
- The nominal test's blind spot is the body: a class writing no body at all passes the test
  once registered by hand with the contract, and the call falls with `AttributeError`.
- The structural test's blind spot is the signature: a class carrying both names but
  demanding an extra argument passes the test, and the call falls with `TypeError`.
- **3** of three blind-spot candidates pass at least one test, and **1** answers the call:
  passing the test does not say the call will be answered.

#### Next Step

Throughout this topic the contract was always declared **in code**. The abstract base
demanded bodies be written and did not let an instance be built when they were not; the
protocol demanded names exist and gave the test's result at runtime. Both stayed incomplete,
but both were **working** checks: what was declared was compared against what existed, and
the result came back as a value.

The last measurement's gap calls for a different kind of declaration. `WrongSignature` passed
the test, because all the test saw was names; **how many arguments** the method takes, what
the arguments look like, and what it returns entered no test at all. This information can be
written into code — Python gives a spelling for it, and the previous lesson's short
definition already used that same spelling as its field declaration. The next topic starts
exactly here: once a contract is put down **in writing**, who tests it, what does runtime do
with that writing, and where does the error surface when what is written and what is done
come apart?

## The Type Layer

Function and variable annotations, reusable type definitions, what the checker finds and cannot find, and schema-based runtime validation.

### Type Hints

Source: https://academia.sh/en/courses/python-object-and-types/type-hints

The annotation is written into the code, and the interpreter stores it without checking it: four of five calls are violations and the number that errors at runtime is zero, yet every annotation remains readable at run time.

The previous lesson declared and tested a contract in code. A test looking for
lineage asked about a class's declared ancestor, a test looking for a method looked
at the names the object actually carried; both gave their result during a **run**,
on the object sitting in hand. The contract was in the code, and the tester was
code too.

This lesson's question is born from another way the contract gets written. Writing
"a string comes in here, an integer comes out here" on a function's signature is
also a contract — but this contract is written not into a class tree or a method
name, it is written into **the writing itself**. The course's axis is the same
here: **who checks the claim?** This lesson measures one layer of the answer, the
lowest one: run time.

#### How an Annotation Is Written

A **type hint** is the contract stating which type of value a name is expected to
carry. An **annotation** is that hint written into the code: after a function
parameter, a colon and a type; for the return, `->` and a type.

```python
def print_age(name: str, age: int) -> str:
    return f"{name}:{age}"
```

The same notation is valid outside a function too. Inside a class body, `name: str`
declares a field; at module level, `counter: int = 0` declares a variable. There is
a fourth place too: a local variable inside a function's body. The measurement
separates these four, because the notation being the same does not mean the result
is the same.

The Programming Fundamentals course established type conversion and dynamic
typing; which object a name binds to at run time was measured there and is not
repeated here. The **TypeScript** course asked the same question too, in a
language whose type layer is checked before execution. The difference here can be
said in one sentence: in Python, an annotation **does not stop execution**; what
this lesson measures is exactly the count of that not-stopping.

#### What the Interpreter Does with an Annotation

The interpreter reads the annotation, evaluates it, and **stores** it. Where it
stores it is the function's `__annotations__` dictionary; the class and the module
have a dictionary of the same name too. This dictionary is an ordinary dictionary
and can be read at run time.

What it does not do is check it. When a call comes in, whether the object bound to
the parameter matches the annotation is not looked at; a non-matching object gets
bound too, and the function body runs too. This has a direct consequence: what is
written in the annotation does not even have to be **a type**. The measurement
shows this too — a function whose return annotation is written as a number is
defined without a problem, called without a problem, and that number sits in the
dictionary exactly as written.

The measurement asks two questions separately. First: wherever an annotation is
written, does it get stored? Second: of calls that run against the annotation, how
many error?

The measurement's assumptions:

- **TL1** — The oracle is the setup itself: which of the five calls carries a type
  violation and which carries a domain violation was decided by the lesson's
  author. The measurement takes this classification from the setup, not from a
  tool.
- **TL2** — A **type violation** is the bound object not being of the annotated
  type. A **domain violation** is the object's type being correct while its value
  breaks the domain rule: an empty name, a negative age.
- **TL3** — Two of the five calls carry a type violation, two carry a domain
  violation, one is clean. The arrays come from this course's shared setup and are
  not changed in this lesson.
- **TL4** — The run-time layer's measure is whether the call raises an exception.
  The body is nothing but a string formatting, and formatting accepts every type;
  what is measured is not what the body does, it is **whether the annotation gets
  in the call's way**.
- **TL5** — In the storage measurement, what is counted is the number of items in
  the `__annotations__` dictionary; the return annotation is an item too.
- **TL6** — Saying "nowhere" for a local variable's annotation means no such
  record exists on the function object; the measure is taken through
  `__annotations__`.
- **TL7** — In the compound-annotation measurement, the only question asked is
  whether the stored object can be given directly to an `isinstance` test. If the
  test itself raises an exception rather than failing, "no" is written.
- **TL8** — The error in the last line is an error the **body** produces, not the
  annotation; the measurement runs that call separately to tell the two apart.

#### Measurement

```python
"""Type hints: where the annotation is stored, what is checked at run time."""
import sys

counter: int = 0


def describe_person(name: str, age: int) -> str:
    return f"{name}:{age}"


class Record:
    name: str
    age: int = 0


def local_annotation():
    temp: int = 1
    return temp


def stored(obj):
    return len(getattr(obj, "__annotations__", {}))


print(f"{'location of annotation':<24s} {'stored where':<26s} {'readable count':>14s}")
LOCATIONS = (("function signature", "function.__annotations__", stored(describe_person)),
          ("class body", "Class.__annotations__", stored(Record)),
          ("module level", "module __annotations__", stored(sys.modules[__name__])),
          ("function body", "nowhere", stored(local_annotation)))
for location, stored_where, count in LOCATIONS:
    print(f"  {location:<22s} {stored_where:<26s} {count:14d}")
print(f"function annotation: {describe_person.__annotations__}")


def trial(x: "not a number annotation") -> 3:
    return x


print(f"non-type annotation stored: {trial.__annotations__}")
print(f"trial(41) -> {trial(41)}")

CALLS = (
    ("name", 30),          # valid
    ("name", "thirty"),    # type violation: age is a string
    (7, 30),               # type violation: name is a number
    ("name", -5),          # type valid, domain violation: age negative
    ("", 30),              # type valid, domain violation: name empty
)


def domain_rule(name, age):
    errors = []
    if isinstance(name, str) and not name:
        errors.append("name empty")
    if isinstance(age, int) and age < 0:
        errors.append("age negative")
    return errors


def runtime(calls):
    errors, results = 0, []
    for arg in calls:
        try:
            results.append(describe_person(*arg))
        except Exception:
            errors += 1
            results.append(None)
    return errors, results


type_violations = [i for i, a in enumerate(CALLS)
              if not isinstance(a[0], str) or not isinstance(a[1], int)]
domain_violations = [i for i, a in enumerate(CALLS) if domain_rule(*a)]
errors, results = runtime(CALLS)

print()
print(f"{'call':>6s} {'type violation':>15s} {'domain violation':>17s} "
      f"{'runtime':>10s} {'returned value':>17s}")
for i, arg in enumerate(CALLS):
    print(f"{i:6d} {str(i in type_violations):>15s} {str(i in domain_violations):>17s} "
          f"{'no error':>10s} {results[i]!r:>17s}")
print()
print(f"calls {len(CALLS)}, violating {len(set(type_violations) | set(domain_violations))}, "
      f"type violation {len(type_violations)}, domain violation {len(domain_violations)}, "
      f"erroring at runtime {errors}")


def total(numbers: list[int]) -> int:
    return sum(numbers)


COMPOUND = (("list[int]", list[int], [1, 2]),
           ("str | None", str | None, "name"),
           ("dict[str, int]", dict[str, int], {"a": 1}),
           ("tuple[int, ...]", tuple[int, ...], (1, 2)))

print()
print(f"{'compound annotation':<18s} {'testable with isinstance':>25s}")
for name, obj, example in COMPOUND:
    try:
        isinstance(example, obj)
        testable = "yes"
    except TypeError:
        testable = "no"
    print(f"  {name:<16s} {testable:>25s}")
print(f"total's annotation: {total.__annotations__}")
try:
    total(["a", "b"])
    print("total(['a', 'b']) -> no error")
except TypeError:
    print("total(['a', 'b']) -> the body errored, not the annotation")
```

```
location of annotation   stored where               readable count
  function signature     function.__annotations__                3
  class body             Class.__annotations__                   2
  module level           module __annotations__                  1
  function body          nowhere                                 0
function annotation: {'name': <class 'str'>, 'age': <class 'int'>, 'return': <class 'str'>}
non-type annotation stored: {'x': 'not a number annotation', 'return': 3}
trial(41) -> 41

  call  type violation  domain violation    runtime    returned value
     0           False             False   no error         'name:30'
     1            True             False   no error     'name:thirty'
     2            True             False   no error            '7:30'
     3           False              True   no error         'name:-5'
     4           False              True   no error             ':30'

calls 5, violating 4, type violation 2, domain violation 2, erroring at runtime 0

compound annotation  testable with isinstance
  list[int]                               no
  str | None                             yes
  dict[str, int]                          no
  tuple[int, ...]                         no
total's annotation: {'numbers': list[int], 'return': <class 'int'>}
total(['a', 'b']) -> the body errored, not the annotation
```

#### Stored and Not Stored

The upper table puts the four locations side by side, and there is a sharp
boundary between three of them and one.

A function signature stores **3** annotations: two parameters and the return.
A class body stores **2**, module level **1**. In these three places, the
annotation turns into an attribute of the object it was written on and can be read
from there while the program runs.

A local annotation inside a function body, by contrast, gives **0**. The line
`temp: int = 1` is a valid Python line, it performs the assignment, it raises no
error — but it leaves no record behind. A local name's lifetime is bound to the
call, and its annotation's lifetime is bound to just as much. Had the same function
been called a thousand times, a thousand separate local names would have been born
and died, and the stored annotation count would still have stayed at **0**. At
class and module level the situation is reversed: the annotation there is
processed once, the moment the definition is made, and it stays for the object's
whole lifetime.

The distinction that follows is the ground the later lessons stand on: **not every
annotation written into the source is visible at run time.** A layer reading the
source sees all four locations; a layer reading the running program sees only
three.

The non-type annotation's line hits the same point from another angle. The `3`
written as the return annotation sits in the dictionary as `3`, and the call
`trial(41)` returns **41**. The interpreter does not look at whether the
annotation is meaningful, because it has no side that interprets the annotation.

#### Four Violations, Zero Errors

The lower table gives the lesson's central number. **Two** of the five calls carry
a type violation: in one, a string stands in for the age, in the other, a number
stands in for the name. **Two** carry a domain violation: age **-5**, name an
empty string. **4** calls total are violations, and one is clean.

The runtime column says "no error" on all five of the five lines. The count is
**0**. The fourth column also shows why: every call returns a value. `'name:thirty'`
gets produced, because formatting accepts a string too. `'7:30'` gets produced,
because formatting accepts a number too. `'name:-5'` and `':30'` get produced,
because **-5** is an integer and an empty string is a string.

**In this measurement, there is no layer catching the violations.** The answer to
who caught them is singular here, and it is empty: run time catches **0**. The
oracle knows the four violations because we wrote the setup; the program does not.

An objection is fair here: the body is lenient because it is nothing but a string
formatting; a different body would have errored on the wrong type. True, but that
is not what is measured. Had such a body errored, what produced the error would
still have been the **operation inside the body**, not the annotation — and the
error would have come after the function was called, with part of the work already
done. The only thing the annotation could have provided is the call **never
starting at all**; in the measurement, no call fails to start.

The two violation kinds being counted separately is not a detail, it is the axis
the rest of the course stands on. The annotation `name: str` does not exclude an
empty string — an empty string is a string. The annotation `age: int` does not
exclude a negative number — **-5** is an integer. **A type being correct does not
mean a value is valid**, and this is not a shortcoming of annotations, it is their
definition: an annotation declares the type, not the value.

#### A Compound Annotation Is Not a Check

An annotation does not have to be a single type name. `list[int]` declares "a list
of integers," `dict[str, int]` "a dict whose keys are strings and values are
integers," `str | None` "a string or nothing," `tuple[int, ...]` "a tuple made of
as many integers as wanted." All four are valid annotations, and all four sit as
an object inside `__annotations__` — the last line shows this: the `total`
function's annotation carries a `list[int]` object.

The third table measures these objects' limit at run time. **Three** of the four
cannot be given directly to an `isinstance` test; the test does not return a
result, it raises an exception. Only `str | None` gives "yes," because a union
merely places two type names side by side and requires no look inside the
content.

Why this is so is not a design question, it is a cost question.
`isinstance(x, list)` is answered with a single glance; `isinstance(x, list[int])`
would mean a thousand tests on a thousand-item list. A test's cost grows with the
container's size, and the language does not silently take on that cost.

The result reinforces the lesson's central claim: **an annotation is not a check,
it is a record.** The last line shows this directly — the call `total(["a", "b"])`
errors, but what produces the error is not the annotation, it is **the summation
operation inside the body**. Had there been no annotation, the same error would
have come out at the same point; even with the annotation present, the call was
not blocked.

#### What Use Is an Annotation That Is Never Checked

The zero raises a question: why write a declaration that is never checked?

The answer sits in the line in the middle of the output. `describe_person.__annotations__`
can be read at run time and gives `str` for `name`, `int` for `age`, `str` for the
return. The annotation is not checked, but it is **not lost either**. It sits in a
dictionary, under a known attribute name, reachable while the program runs.

This is the fact the next three lessons rest on entirely. A program can be written
that reads the annotation and looks for a type mismatch; a program can be written
that reads the source without ever running it and does the same job; a layer can
be written that uses the annotation like a schema and checks the value at call
time. All three take this dictionary as raw material.

There is a second benefit, for the reader. The signature `def
describe_person(name, age)` does not say in what order the caller should give
what; `def describe_person(name: str, age: int) -> str` does. This is not a check,
it is a **document** — and being a document, it can also be wrong. There is no
mechanism ensuring the annotation and the body agree; what ensures agreement is
**another layer** reading the annotation.

The record itself is not protected either. `__annotations__` is an ordinary
dictionary: it can be modified just as it can be read. A running program can
delete a function's annotation or replace it with a different type; the
function's body is unaffected by this, and calls keep giving the same result.
This does not mean the record is unreliable — it means the record is **not
binding**. Bindingness is only born once a layer is added that reads the record
and makes a decision, and it is the rest of this course that builds that layer.

#### Summary

- A type hint is the contract stating what type of value a name will carry; an
  annotation is that hint written into the code, and it can be written on a
  function signature, a class body, module level, and a function body.
- An annotation is stored in three places and not stored in one: a function gives
  **3** items, a class **2**, a module **1**; a local annotation inside a function
  body gives **0** and leaves no record behind.
- The interpreter stores the annotation, it does not check it; the annotation
  does not even have to be a type — a number written as a return annotation sits
  in the dictionary exactly as written, and the call runs.
- **4** of five calls are violations — **2** type violations, **2** domain
  violations — and the number that errors at run time is **0**. There is no layer
  in this measurement catching the violations.
- **Three** of four compound annotations cannot be given directly to an
  `isinstance` test; an annotation is not a check, it is a record, and the record
  is not binding.
- A type being correct does not mean a value is valid: an empty string is a
  string, **-5** is an integer. An annotation declares the type, not the value.

#### Next Step

The `describe_person` function's annotation carries two concrete type names, and so
declares a contract that only works with those two types. Yet a great many
functions are indifferent to type: a function giving a list's first item does not
care what the list is filled with, but it does know that **what it returns is the
same type as what is inside the list**. This bond cannot be written with a
concrete type name — writing it either loses the bond or needlessly narrows the
function. The next lesson lays four separate notations of the same function side
by side and asks a single number: how many times can each notation's annotation
declare a call's return type?

### Generics and Type Variables

Source: https://academia.sh/en/courses/python-object-and-types/generics-and-type-variables

Four notations of the same function give different results at four call sites: the unannotated notation misses two violations, the loose notation finds both but produces two false alarms, the generic notation finds both and produces no false alarm at all.

The previous lesson measured that an annotation gets stored and does not get
checked; four of five calls were violations and the number that errored at run
time was zero. The measured function's annotation carried two concrete type
names: `name: str`, `age: int`. Such a signature declares a contract that only
works with those two types, and what it declares is explicit.

A great many functions, though, are indifferent to type. A function giving a
list's first item does not care what the list is filled with — but even not
caring, it knows one thing: **what it returns is the same type as what is inside
the list.** This bond cannot be written with a concrete type name. This lesson's
question is whether that bond can be written at all, and what it earns you when
it is. The measure is again the course's axis: **which notation can declare which
violation?**

#### The Two Ends of a Concrete Type Name

There are two easy ways to write the `first` function, and both cost you
something.

The first way is to **loosen** the annotation: `def first(seq: list) -> object`.
This signature accepts every list, it is correct, and it rejects no call
needlessly. What it loses is the bond — saying "an object" for the return says
nothing about the value in the caller's hand.

The second way is to **concretize** the annotation: `def first(seq: list[int]) ->
int`. This signature declares the return exactly. What it loses is the function
itself — even though the body works with every list, the signature only accepts
lists of integers, and a valid call made with a list of strings is also counted a
violation.

There seems to be no middle ground between the two: either you give up the bond,
or you narrow the function. Generics are exactly the notation that removes this
dilemma.

#### The Type Variable

A **type variable** is a name that stands in for a type name. The line `T =
TypeVar("T")` produces such a name; `T` is not itself a type, it marks **where a
type will stand** in the signature.

```python
from typing import TypeVar

T = TypeVar("T")


def first(items: list[T]) -> T:
    return items[0]
```

This signature says two things. First: the argument is a list filled with any
type — the accepted set did not narrow. Second: the return is of **the same**
type as what is inside that list. The same name appearing in two places is what
creates the bond; whatever `T` is in one spot, it is that in the other too.

A structure whose signature carries the same name is called **generic**. A
generic can be on a function just as it can be on a class: a class deriving from
`Generic[T]` can spread the type of the value it carries into its own
signatures.

A type variable does not have to be left completely free. A **bound** type
variable declares an upper limit — `TypeVar("Text", bound=str)` only accepts
strings and their subclasses. A **constrained** type variable, by contrast,
declares a finite list of options: `TypeVar("Numeric", int, float)`. Both kinds
of limit can be read at run time; the measurement shows the difference between
these limits being readable and being checked, too.

#### The Measurement's Setup

The measurement compares four notations at four call sites. Every call site
consists of two things: a call to `first`, and an operation applied to its
result. The operation **expects** a type from the result — `+ 1` expects an
integer, `.upper()` expects a string. A call site being a violation means the
list's item type does not match the type the operation expects.

The measurement's assumptions:

- **TL9** — The oracle is the setup itself: which of the four call sites is a
  violation is decided by the list's item type and the type the operation
  requires; both were written by the lesson's author.
- **TL10** — **Two** of the four call sites are violations. The set is kept
  small, because what is measured is not the violation rate, it is **the set of
  violations each notation can declare**.
- **TL11** — The inference step is modeled within the lesson: it produces a call
  site's return type from a return annotation and substitutes the type variable
  with the argument's item type. A full checker is not built in this lesson.
- **TL12** — "Declared" means the notation reports a problem at that call site;
  it happens for one of two reasons: the argument does not match the annotation,
  or the inferred return type is other than what the operation expects.
- **TL13** — "Correct" means the declaration really lands on a violating call
  site. "Missed" means the violation was not declared. "False alarm" means a
  declaration was made at a valid call site.
- **TL14** — An `object` return is never counted equal to any operation's
  expected type; the loose notation declaring at all four call sites is a
  consequence of this rule.
- **TL15** — The runtime column is measured by **actually running** the four
  call sites, and only type-originated exceptions are counted.
- **TL16** — In the generic class measurement, identity is shown with `is`; no
  identity number is printed.
- **TL17** — The measure for bound and constrained type variables is whether the
  bound is readable and whether a call breaking the bound runs. In the
  two-argument measurement, the unification step is also modeled within the
  lesson: the type variable binds on its first match, and consistency of the
  bond is tested at subsequent matches.

#### Measurement

```python
"""Generics: which notation can declare which violation."""
from typing import Generic, TypeVar

T = TypeVar("T")


def first(items):
    return items[0]


NOTATIONS = (("no annotation", None, None),
            ("list -> object", list, object),
            ("list[int] -> int", list[int], int),
            ("list[T] -> T", list[T], T))

# call site: the source list's item type, the type the following operation requires
SITES = (("first([1, 2]) + 1", int, int),
          ("first(['a', 'b']) + 1", str, int),
          ("first(['a', 'b']).upper()", str, str),
          ("first([1, 2]).upper()", int, str))


def accepts(param, item):
    """Does the annotation accept this call site's argument."""
    inner = getattr(param, "__args__", (None,))[0]
    return param is None or param is list or inner is T or inner is item


def infer(ret, item):
    """Can the call site's return type be inferred from the return annotation."""
    if ret is None:
        return None
    return item if ret is T else ret


print(f"{'notation':<18s} {'inferred':>10s} {'declared':>11s} {'correct':>6s}"
      f" {'missed':>10s} {'false alarm':>13s}")
for name, param, ret in NOTATIONS:
    inferred = declared = correct = missed = false_alarm = 0
    for site, item, expected in SITES:
        real_violation = item is not expected
        c = infer(ret, item)
        inferred += c is not None
        flag = (not accepts(param, item)) or (c is not None and c is not expected)
        declared += flag
        correct += flag and real_violation
        missed += real_violation and not flag
        false_alarm += flag and not real_violation
    print(f"  {name:<16s} {f'{inferred}/4':>10s} {declared:11d} {correct:6d}"
          f" {missed:10d} {false_alarm:13d}")

REAL = (lambda: first([1, 2]) + 1,
          lambda: first(["a", "b"]) + 1,
          lambda: first(["a", "b"]).upper(),
          lambda: first([1, 2]).upper())
failing = 0
for fn in REAL:
    try:
        fn()
    except (TypeError, AttributeError):
        failing += 1
print(f"call sites {len(SITES)}, real violations "
      f"{sum(o is not g for _, o, g in SITES)}, "
      f"erroring at runtime {failing}")


def combine(a, b):
    return a if a == b else b


PAIRS = ((int, int), (int, str), (str, str))
SIGNATURES = (("a: object, b: object", (object, object)),
           ("a: T, b: object", (T, object)),
           ("a: T, b: T", (T, T)))


def unify(signature, types):
    """Binds the type variable on first match, then tests consistency."""
    binding = {}
    for annotation, type_ in zip(signature, types):
        if annotation is T:
            if T in binding and binding[T] is not type_:
                return False
            binding[T] = type_
        elif annotation is not object and annotation is not type_:
            return False
    return True


print()
print(f"{'two-argument signature':<24s} {'declared':>11s}")
for name, signature in SIGNATURES:
    print(f"  {name:<22s} "
          f"{sum(not unify(signature, c) for c in PAIRS):11d}")
print(f"pairs {len(PAIRS)}, with differing types "
      f"{sum(x is not y for x, y in PAIRS)}, "
      f"combine(1, 'a') -> {combine(1, 'a')!r} (runtime raised no error)")


class Box(Generic[T]):
    def __init__(self, value: T):
        self.value = value

    def get(self) -> T:
        return self.value


b = Box[int]("string")
print()
print(f"Box[int]('string').get() -> {b.get()!r}")
print(f"type(b) is Box -> {type(b) is Box}; "
      f"Box[int] is Box[str] -> {Box[int] is Box[str]}")
print(f"isinstance(b, Box) -> {isinstance(b, Box)}")
try:
    isinstance(b, Box[int])
    print("isinstance(b, Box[int]) -> tested")
except TypeError:
    print("isinstance(b, Box[int]) -> cannot be tested")
print(f"get's annotation: {Box.get.__annotations__}")

Text = TypeVar("Text", bound=str)
Numeric = TypeVar("Numeric", int, float)
print()
print(f"bound type variable's bound: {Text.__bound__}")
print(f"constrained type variable's options: {Numeric.__constraints__}")


def widen(d: Text) -> Text:
    return d


print(f"widen(7) -> {widen(7)!r} (the bound was not checked at runtime)")
```

```
notation             inferred    declared correct     missed   false alarm
  no annotation           0/4           0      0          2             0
  list -> object          4/4           4      2          0             2
  list[int] -> int        4/4           3      2          0             1
  list[T] -> T            4/4           2      2          0             0
call sites 4, real violations 2, erroring at runtime 2

two-argument signature      declared
  a: object, b: object             0
  a: T, b: object                  0
  a: T, b: T                       1
pairs 3, with differing types 1, combine(1, 'a') -> 'a' (runtime raised no error)

Box[int]('string').get() -> 'string'
type(b) is Box -> True; Box[int] is Box[str] -> False
isinstance(b, Box) -> True
isinstance(b, Box[int]) -> cannot be tested
get's annotation: {'return': ~T}

bound type variable's bound: <class 'str'>
constrained type variable's options: (<class 'int'>, <class 'float'>)
widen(7) -> 7 (the bound was not checked at runtime)
```

#### Four Notations, Four Results

The upper table's last three columns count three independent defects: a missed
violation, a false alarm, and an uninferrable return type. Only one of the four
notations zeroes out all three at once.

**The unannotated notation** infers no return type at all: **0/4**. The
declaration count is **0**, missed is **2**. False alarm is also **0** — a
signature that says nothing cannot be mistaken either. This row is a baseline:
what the three notations below earn is measured by how far they move from this
row.

**The loose notation** infers a return type at all four of the four call sites:
**4/4**. But the type it infers is `object` every time, and no operation expects
`object`. Declared **4**, correct **2**, false alarm **2**. A notation declaring
at every call site is a notation telling no call site apart from another. Missed
dropped to **0**, and in exchange, false alarm rose from **0** to **2**.

**The overly concrete notation** makes three declarations: **2** correct, **1**
false alarm. The false alarm comes from the valid call made with a list of
strings — the signature rejects that call at the argument level, even though
the body works with it without a problem. One of the correct declarations comes
from the same place too, meaning it gives the right result for the wrong
reason: the violation sits on the return side, while the signature blames the
argument.

**The generic notation** infers the return type at all four of the four call
sites, makes **2** declarations, both correct, missed **0**, false alarm **0**.
It zeroes missed the way the loose notation does, and zeroes false alarm the way
the unannotated notation does. Both at once, in a single row.

The gain's source sits in one place: **the bond**. The other three notations
build no relationship between the argument and the return — each gets a
separate type written on it, or none at all. The generic notation declares the
relationship by using a single name in two places, and inference can produce a
call-site-specific answer from that relationship.

#### Using the Same Name in Two Arguments

The bond's power is not limited to the return. The second table measures a
two-argument function: `combine`, which compares two values and returns one of
them. If the contract is to say "the two arguments must be of the **same**
type," the only way to write that is using the same type variable in two
parameters.

The measurement models this with a **unification** step: the type variable
binds to a type on its first match, and if the same variable is seen at the
next parameter, whether the bond holds is checked.

Three signatures, three pairs. The signature written with `object` makes **0**
declarations — there is a pair with differing types, but the signature says
nothing that could tell them apart. `a: T, b: object` also gives **0**: when the
type variable appears in only one spot, no bond forms, because a bond **needs
two ends**. `a: T, b: T`, by contrast, makes **1** declaration, and that is the
one real mismatch in the set.

The last line answers the "who caught it" question again: the call `combine(1,
"a")` raises no error, it returns a value. **Run time does not see this
mismatch**; the only thing that sees it is the repeated name in the signature.

#### Who Caught It in This Measurement

The layer making the declarations is **the side reading the annotation**, and in
this lesson only its inference step was modeled; a full checker is the subject
of the next lesson.

The runtime row gives a separate result: **2** errors come out at the four call
sites, meaning both real violations do get caught. This does not contradict the
previous lesson's **0** result — there, the body was a formatting that accepted
every type; here, the operation outside the body really uses the type. The
difference sits at three points. Run time gives the error **after the call**,
once `first` has already returned. It only gives it on lines that actually
**run**; had one of the four call sites sat inside a condition that was never
satisfied, that violation would have stayed invisible. And what produces the
error is not the annotation, it is the operation itself — had the annotation
been deleted, the same two errors would have come out at the same places.

The generic notation's **2** declarations, by contrast, are produced without
running anything, purely by looking at the call site. The two layers catch the
same violations here; **when** they catch them is what differs.

#### What Happens to a Generic Class at Run Time

The lower blocks measure generics' run-time counterpart, and the result confirms
the previous lesson's claim.

`Box[int]("string")` is a type violation: the box is declared to carry an
integer, and a string is put inside it. The call runs, `get()` gives the string
back, no error. `type(b) is Box` is **true** — the produced object's class is
`Box`; the type in the square brackets does not get baked into the object's
class. `Box[int] is Box[str]` is **false**: the two are separate objects, but
both produce instances of the same class.

`isinstance(b, Box)` gives **true**; `isinstance(b, Box[int])`, by contrast,
cannot be tested. The same result as `list[int]` in the previous lesson: a
parameterized annotation cannot be given directly to a run-time test. The `get`
method's annotation also carries the type variable itself — not a resolved
type, but the name of the spot to be resolved.

The last two lines measure bound and constrained type variables. The bounds
can be read: the bound variable's upper limit is `str`, the constrained
variable's options are `int` and `float`. But the call `widen(7)`, which breaks
the bound, runs and returns **7**. **A bound is an annotation too:** it gets
recorded, it gets read, it does not get checked.

#### Summary

- A type variable is a name standing in for a type name; the same name appearing
  twice in a signature builds a **bond** between argument and return, and a
  generic is the notation carrying that bond.
- Four notations diverge at four call sites: the unannotated notation gives
  **0** declarations and **2** missed, the loose notation gives **4**
  declarations and **2** false alarms, the overly concrete notation gives **3**
  declarations and **1** false alarm.
- The generic notation makes **2** declarations, both correct; missed and false
  alarm are **0**. The gain's source is the bond, not the concreteness of the
  type names.
- A bond needs two ends: when the same type variable appears in two parameters,
  a mismatch is declared (**1**); when it appears in a single parameter, or when
  `object` is written, it is not declared (**0**).
- A bound type variable declares an upper limit, a constrained type variable a
  finite set of options; both kinds of limit can be read at run time but are not
  checked.
- A generic class's type in square brackets does not get baked into the
  instance's class: `type(b) is Box` is true, while an instance test made with
  `Box[int]` cannot be performed.

#### Next Step

In this lesson, only one step of the side reading the annotation was modeled:
inferring a call site's return type. A real static checker does more than this —
it scans every call, compares each one's arguments against the signature, and
declares the mismatches without running anything. The next lesson models that
checker within the lesson and applies it to the previous lesson's five calls.
The number asked is this: of the four violations, how many does it find, how
many does it miss — and why can it not find the ones it misses?

### Static Type Checking

Source: https://academia.sh/en/courses/python-object-and-types/static-type-checking

The checker modeled within the lesson finds two of five calls' type violations and cannot see two domain violations; in exchange, it sees all three of the source's call sites, while run time only reaches one.

The previous lesson modeled a single step of the side reading the annotation:
inferring a call site's return type. The measurement showed that the generic
notation could declare both violations with no false alarm — but there was no
program making the declaration, only an inference rule.

This lesson builds that program. A layer that reads the source or the
annotations, compares calls against the signature, and declares mismatches
**without running them** is called **static type checking**. The number asked
has three parts: of the first lesson's four violations, how many does this layer
find, how many does it miss, and why does it miss the ones it misses?

#### The Checker Is Modeled in This Lesson

A warning up front: the checker used in this lesson is **written within the
lesson**. No ready-made tool is run, and no tool's name appears. What is
measured is not a specific program's behavior, it is **the approach's own
limit** — and that limit follows from the information in the checker's hand.
The modeled checker uses exactly that information: it reads the annotations,
looks at the call's arguments, and lists the mismatches.

The measurement uses two checker forms. The first takes the calls' arguments
directly; because the arguments are constants written into the source, this
gives the same information as reading the source. The second does the job more
explicitly: it turns the source text into an **abstract syntax tree**, walks the
call nodes in the tree, and compares the constant arguments against the
signature. The second **never runs** the code at all; this is the measurable
counterpart of the word "static."

#### The Information in the Checker's Hand

The checker sees three things: the annotations in the signature, the call
sites, and the constant values written into the calls. There is exactly one
thing it does not see, and the whole lesson is the consequences of that one
thing: it does not see **the value's meaning**.

The annotation `age: int` says "an integer comes here." `-5` is an integer.
The checker performs the test `isinstance(-5, int)`, gets the answer **true**,
and passes it. In the same way, the annotation `name: str` does not exclude an
empty string, because an empty string is a string. This is not a shortcoming, it
is exactly what is written in the signature: **the signature declares the type,
not the domain.**

The measurement's assumptions:

- **TL18** — The oracle is the setup itself: which of the five calls carries a
  type violation and which carries a domain violation was decided in the first
  lesson and is not changed here.
- **TL19** — The checker is modeled within the lesson; no external tool is run.
  The model's rule is singular: if the type of the value bound does not match
  the type in the annotation, declare it.
- **TL20** — The domain rule is also modeled within the lesson and looks at
  only two conditions: is the name empty, is the age negative. These two
  conditions are the setup's definition of a domain violation.
- **TL21** — In the scope measurement, the checked code sits as a text
  constant; it is checked both by being turned into an abstract syntax tree and
  by being run. The two layers are applied to **the same text**.
- **TL22** — "Executed call site" is the number of calls really reached during
  a run, and it is counted with a tracked wrapper. The run is done in a single
  mode.
- **TL23** — The abstract-syntax-tree check only tests **constant** arguments;
  an argument given as a variable is not checked in this model and is not
  counted.
- **TL24** — In the domain-type measurement, the `Age` class derives from
  integer and tests its rule **at construction time**; the class's instance
  behaves like an integer everywhere.
- **TL25** — "Declared on the raw call" is how many of the five calls, as
  written in the source, get declared; what is counted is the call, not the
  parameter.
- **TL26** — "Erroring during construction" is the number of calls that raise
  an exception when the value is constructed through `Age`.
- **TL27** — No environment-dependent data is written; line numbers are the
  checked text constant's own lines.

#### Measurement

```python
"""Static type checking: what a modeled checker finds, what it cannot see."""
import ast


def describe_person(name: str, age: int) -> str:
    return f"{name}:{age}"


CALLS = (
    ("name", 30),          # valid
    ("name", "thirty"),    # type violation: age is a string
    (7, 30),               # type violation: name is a number
    ("name", -5),          # type valid, domain violation: age negative
    ("", 30),              # type valid, domain violation: name empty
)


def checker(function_, calls):
    """Modeled checker reading annotations and checking calls.

    Only sees TYPE mismatch; cannot see the value's meaning.
    """
    signature = [(a, t) for a, t in function_.__annotations__.items() if a != "return"]
    found = []
    for i, arg in enumerate(calls):
        for (name, type_), value in zip(signature, arg):
            if not isinstance(value, type_):
                found.append((i, name))
    return found


def domain_rule(name, age):
    """Meaning conditions the type cannot see."""
    errors = []
    if isinstance(name, str) and not name:
        errors.append("name empty")
    if isinstance(age, int) and age < 0:
        errors.append("age negative")
    return errors


found = checker(describe_person, CALLS)
type_violations = [i for i, a in enumerate(CALLS)
              if not isinstance(a[0], str) or not isinstance(a[1], int)]
domain_violations = [i for i, a in enumerate(CALLS) if domain_rule(*a)]

print(f"{'call':>6s} {'type violation':>15s} {'domain violation':>17s} "
      f"{'checker found':>15s} {'runtime':>10s}")
for i, arg in enumerate(CALLS):
    print(f"{i:6d} {str(i in type_violations):>15s} {str(i in domain_violations):>17s} "
          f"{str(any(b[0] == i for b in found)):>15s} {'no error':>10s}")
print(f"calls {len(CALLS)}, type violation {len(type_violations)}, domain violation "
      f"{len(domain_violations)}, checker found {len({b[0] for b in found})}")
print(f"parameters the checker flagged: {found}")

SOURCE = '''
def dispatch(mode):
    if mode == "short":
        return describe_person("name", 30)
    if mode == "long":
        return describe_person("name", "thirty")
    return describe_person(7, 30)
'''


def static_check(source, function_):
    """Modeled checker reading source text and checking calls.

    Does not run the code; only walks the syntax tree.
    """
    signature = [(a, t) for a, t in function_.__annotations__.items() if a != "return"]
    sites, found = [], []
    for node in ast.walk(ast.parse(source)):
        if isinstance(node, ast.Call) and getattr(node.func, "id", "") == "describe_person":
            sites.append(node.lineno)
            for arg, (name, type_) in zip(node.args, signature):
                if isinstance(arg, ast.Constant) and not isinstance(arg.value, type_):
                    found.append((node.lineno, name))
    return sites, found


PASSED = []


def tracked(name, age):
    PASSED.append((name, age))
    return describe_person(name, age)


env = {"describe_person": tracked}
exec(SOURCE, env)
errors = 0
try:
    env["dispatch"]("short")
except Exception:
    errors += 1

sites, static = static_check(SOURCE, describe_person)
print()
print(f"call sites in source {len(sites)}, executed {len(PASSED)}, "
      f"runtime errors {errors}")
print(f"call sites the checker saw {len(sites)}, violations found "
      f"{len({y for y, _ in static})}, sites found {static}")

SOURCE2 = '''
def forward(value):
    return describe_person("name", value)
'''
sites2, static2 = static_check(SOURCE2, describe_person)
print(f"call written with a variable: site seen {len(sites2)}, "
      f"arguments tested 0, declared {len(static2)}")


class Age(int):
    """A value object carrying the domain rule into the type: construction tests it."""

    def __new__(cls, value):
        if value < 0:
            raise ValueError("age negative")
        return super().__new__(cls, value)


def describe_person_domain(name: str, age: Age) -> str:
    return f"{name}:{age}"


raw = len({b[0] for b in checker(describe_person_domain, CALLS)})
constructed, construction_errors = [], 0
for name, age in CALLS:
    try:
        constructed.append((name, Age(age)))
    except (ValueError, TypeError):
        construction_errors += 1
after_construction = len({b[0] for b in checker(describe_person_domain, constructed)})
print()
print(f"domain-typed signature: declared on raw calls {raw}, erroring during construction "
      f"{construction_errors}, declared on {len(constructed)} constructed calls {after_construction}")
print(f"is Age(30) an integer: {isinstance(Age(30), int)}; "
      f"empty name is still invisible: {checker(describe_person_domain, [('', Age(30))])}")
```

```
  call  type violation  domain violation   checker found    runtime
     0           False             False           False   no error
     1            True             False            True   no error
     2            True             False            True   no error
     3           False              True           False   no error
     4           False              True           False   no error
calls 5, type violation 2, domain violation 2, checker found 2
parameters the checker flagged: [(1, 'age'), (2, 'name')]

call sites in source 3, executed 1, runtime errors 0
call sites the checker saw 3, violations found 2, sites found [(7, 'name'), (6, 'age')]
call written with a variable: site seen 1, arguments tested 0, declared 0

domain-typed signature: declared on raw calls 5, erroring during construction 2, declared on 3 constructed calls 1
is Age(30) an integer: True; empty name is still invisible: []
```

#### Two Found, Two Not Found

The upper table gives the lesson's binding number. **2** of five calls carry a
type violation, **2** carry a domain violation. The number of calls the checker
finds is **2**, and the calls it finds are exactly the ones carrying a type
violation: `age` in the first call, `name` in the second. On the two calls
carrying a domain violation, the column says **false**.

**Who caught it:** in this measurement, **the checker** caught two violations,
and **no layer** caught the other two. The runtime column is clean on all five
lines.

The reason for the ones not found can be read in a single line. In the third
call, the age is **-5**, and `-5` is an integer; `isinstance(-5, int)` returns
true. In the fourth call, the name is an empty string, and an empty string is a
string; `isinstance("", str)` returns true. The checker did not run wrong — it
answered the question it was asked correctly. The question not asked was "is
this value valid," and that question is not written in the signature.

The list of parameters the checker points to also gives the shape of this
information limit. The declaration carries not only the call number but also
**which parameter** did not match: `age` in the first call, `name` in the
second. The declaration can be this specific because a type name is written
right next to that parameter in the signature. Because nothing is written
across from the domain rule, no such declaration can be produced either.

#### The Scope Is the Written Code, Not the Executed Code

The second measurement shows what the checker gives in return, and the number
is striking.

In the checked text, there are **3** call sites for `describe_person`; two are
violations. When the program is run in a single mode, only **1** of these three
is actually reached, because the other two sit inside conditions that are not
satisfied. The runtime error count is **0** — both because the reached call is
clean, and because, as measured in the previous lesson, the annotation does not
stop the call anyway.

The checker walking the abstract syntax tree, by contrast, sees all **3** of
the three call sites and declares **2** violations — along with the line
numbers, and which parameter did not match. It never ran the code:
`ast.parse` turned the text into a tree, `ast.walk` walked the tree, no body was
executed.

The difference between the two is not a percentage, it is a difference of
**kind**. Run time can only examine the site it reaches; reaching it depends on
conditions, input, and the mode of that particular run. The checker's scope,
by contrast, is the text itself: every call site written even once is checked,
even if it is never run. The ratio in the measurement is **3/3** against
**1/3**.

The ratio's meaning changes with the run. Had the same program been run in
three separate modes, all three call sites would have been reached, and the
second and third calls still would have raised no error — because the
annotation does not stop the call. So the number **1/3** is not a measure of
the checking, it is a measure of the **scope**: it says which piece of code
was looked at, not whether what was looked at was found correct.

This does not make up for the two violations the checker cannot find, but it
trades off against them. The checker knows **less**, and it knows that less in
**more places**; run time knows **more** — it sees the real value in hand —
and it knows it in **fewer places**.

#### The Model's Own Limit

One line sits below this table, and honesty requires counting it separately.
In a call whose argument is not a constant but a **variable**, the checker sees
the call site — **1** — but the argument it tests is **0** and the violation it
declares is **0**.

This is not the same kind of limit as the previous section's. Not finding the
domain violation was a **knowledge** limit: because the rule is written nowhere,
no checker can read it. Not being able to test the variable call, by contrast,
is a **model** limit: which value the variable carries could be inferred by
tracing the assignments made to it. The checker modeled in this lesson does not
do that tracing, and the limit is written explicitly here.

The distinction matters, because the remedy for the two is different. A model
limit closes with **a better checker**. A knowledge limit does not close —
without writing the rule down somewhere, no checker can find it. The next
section tries what that "somewhere" could be.

#### Moving the Domain Rule into the Type

The third measurement tries a natural question: if the checker has no domain
information in hand, what happens if we **put** that information into the
signature? The measurement builds an `Age` class deriving from integer; the
class tests its rule at construction time, and its instance behaves like an
integer everywhere — the last line shows this with `isinstance`.

Once the signature is `age: Age`, the checker really changes. **5** of the five
calls, as written in the source, get declared: no constant number is an `Age`
instance, not even the clean call. So putting domain information into the
signature **forces every call site to construct the value.**

Once the values are constructed, the table settles into place. **2** of the
five calls error during construction — negative age and non-integer age, both
fall in the `Age` constructor. Of the remaining **3** calls, **1** is declared
by the checker: the call whose name is a number.

But the last line shows the gap that remains: an empty name is still
invisible, because no such type was built for `name`. Three of four
violations are now caught, and the catchers are separate layers — **1** by the
checker, **2** by construction.

The result that follows from this is this lesson's result, and the next
lesson's starting point. Moving the domain rule into the type makes the
violation visible, but **the place it becomes visible is not the checker, it is
run time**: the call `Age(-5)` raises an exception, and that exception comes
out while the program runs. The rule moved not into the writing, but into a
constructor's body.

#### Summary

- Static type checking is a layer that reads code without running it and
  compares calls against the signature; in this lesson, the checker is modeled
  within the lesson and no external tool is used.
- The checker finds **2** type violations in five calls and cannot see **2**
  domain violations. The reason is missing information: `-5` is an integer, an
  empty string is a string, and the signature does not write a domain rule.
- The check's scope is the written code: the checker walking the abstract
  syntax tree sees all **3** of three call sites and declares **2**
  violations, while the run only reaches **1** call site and raises **0**
  errors.
- The checker knows less, in more places; run time knows more, in fewer
  places; the two do not substitute for each other.
- The two limits are of different kinds: not being able to test a variable
  call is a model limit and closes with a better checker; not being able to see
  a domain rule is a knowledge limit and does not close until the rule is
  written somewhere.
- Once the domain rule is moved into the type, **5** of the raw calls get
  declared, **2** error during construction, and **1** of the three constructed
  calls is caught by the checker — the rule has moved from the writing into a
  constructor's body, meaning into run time.

#### Next Step

The third measurement moved the rule into run time, but did it for a single
domain, with a hand-written class. Writing a separate class for every domain
does not scale; as the number of rules grows, constructors repeat each other,
and where the rules live gets scattered. It is also possible to gather the
rules in one place, as the data's **schema**, and apply that schema at call
time. The next lesson models this layer within the lesson and asks the
course's final number: how many violations does run time, the checker, and the
domain rule catch **separately** — and how many layers does it take to see all
four violations?

### Runtime Validation

Source: https://academia.sh/en/courses/python-object-and-types/runtime-validation

Three layers see the four violations separately: runtime catches 0, the checker 2, the domain rule 2, and all four only become visible when two layers are used together; schema-based validation catches all four, but only on the calls it reaches.

The previous lesson moved a domain rule into the type and showed that the rule
fell into a constructor's body, meaning into run time. Writing a separate class
for every domain does not scale: as the number of rules grows, constructors
repeat each other, and where the rules live gets scattered.

The name for gathering rules in one place is a **schema**: which field is which
type, and which condition it has to satisfy, sits in a definition separate from
the data. **Runtime validation** applies that definition to the value at call
time. This lesson builds that layer and asks the course's final number: how
many layers does it take to see all four violations?

#### The Schema Is Modeled in This Lesson

One warning, the same as the previous lesson's: the schema and the validator
here are **written within the lesson**. No ready-made validation layer is used,
and no tool's name appears. What is measured is not a program's behavior, it is
**the approach's scope**.

Schema validation itself is not new to this course. The Data Modeling and
Relational Theory course built the schema on the data side with integrity
constraints; the Web API Design course built domain-level error reporting in
request and response bodies too. **Those procedures are not repeated here.**
The question here is narrow and measurable: **which layer catches which
violation?**

The measurement's assumptions:

- **TL28** — The oracle is the setup itself: two of five calls carry a type
  violation, two carry a domain violation, one is clean. The array comes from
  the course's shared setup and is not changed.
- **TL29** — The checker and the domain rule are the same models as the
  previous lessons'; the checker only sees the type, the domain rule only the
  value's meaning.
- **TL30** — The schema holds three things in one place: field name, expected
  type, and field condition. The test looks at the type first, and only moves
  to the condition if the type passes; this order is a design decision and it
  shows up in the messages.
- **TL31** — Validation is applied as a layer wrapping the function: if the
  test does not pass, **the call never starts at all**. The catching measure is
  whether this wrapper raises an exception.
- **TL32** — The runtime column is whether the unwrapped call raises an
  exception; the body is a string formatting and accepts every type.
- **TL33** — The "together" count is the union of the calls the two layers
  catch, not their sum.
- **TL34** — In the scope measurement, a run is assumed where three of the five
  calls are executed; which three is written into the setup, and the
  measurement counts this not as a probability but as a single run.
- **TL35** — No environment-dependent data is written; every number comes from
  the setup and the run.

#### Measurement

```python
"""Runtime validation: how many layers does it take to see four violations."""


def describe_person(name: str, age: int) -> str:
    return f"{name}:{age}"


CALLS = (
    ("name", 30),          # valid
    ("name", "thirty"),    # type violation: age is a string
    (7, 30),               # type violation: name is a number
    ("name", -5),          # type valid, domain violation: age negative
    ("", 30),              # type valid, domain violation: name empty
)


def checker(function_, calls):
    """Modeled checker reading annotations: sees only the type."""
    signature = [(a, t) for a, t in function_.__annotations__.items() if a != "return"]
    found = []
    for i, arg in enumerate(calls):
        for (name, type_), value in zip(signature, arg):
            if not isinstance(value, type_):
                found.append((i, name))
    return found


def domain_rule(name, age):
    """Meaning conditions the type cannot see."""
    errors = []
    if isinstance(name, str) and not name:
        errors.append("name empty")
    if isinstance(age, int) and age < 0:
        errors.append("age negative")
    return errors


# The schema is modeled within the lesson: field name, type, and domain rule in one place.
SCHEMA = (("name", str, lambda d: bool(d), "empty"),
        ("age", int, lambda d: d >= 0, "negative"))


def validate(schema, values):
    """Schema-based validation: tests both the type and the domain at call time."""
    errors = []
    for (field, type_, rule, message), value in zip(schema, values):
        if not isinstance(value, type_):
            errors.append(f"{field}: type")
        elif not rule(value):
            errors.append(f"{field}: {message}")
    return errors


def guarded(schema, function_):
    """Layer wrapping the function with the schema: if validation fails, the call never starts."""
    def wrapper(*args):
        errors = validate(schema, args)
        if errors:
            raise ValueError("; ".join(errors))
        return function_(*args)
    return wrapper


found = {b[0] for b in checker(describe_person, CALLS)}
type_violations = [i for i, a in enumerate(CALLS)
              if not isinstance(a[0], str) or not isinstance(a[1], int)]
domain_violations = [i for i, a in enumerate(CALLS) if domain_rule(*a)]
guarded_fn = guarded(SCHEMA, describe_person)

raw_errors = 0
schema_caught = set()
for i, arg in enumerate(CALLS):
    try:
        describe_person(*arg)
    except Exception:
        raw_errors += 1
    try:
        guarded_fn(*arg)
    except ValueError:
        schema_caught.add(i)

print(f"{'call':>6s} {'type violation':>15s} {'domain violation':>17s} "
      f"{'runtime':>10s} {'checker':>10s} {'domain rule':>12s} "
      f"{'schema':>7s}")
for i, arg in enumerate(CALLS):
    print(f"{i:6d} {str(i in type_violations):>15s} {str(i in domain_violations):>17s} "
          f"{'no error':>10s} {str(i in found):>10s} "
          f"{str(i in domain_violations):>12s} {str(i in schema_caught):>7s}")
print()
print(f"calls {len(CALLS)}, violating "
      f"{len(set(type_violations) | set(domain_violations))} | caught by: runtime "
      f"{raw_errors}, checker {len(found)}, domain rule {len(domain_violations)}, "
      f"schema {len(schema_caught)}")
print(f"checker and domain rule together: "
      f"{len(found | set(domain_violations))}")
print(f"schema messages: {[validate(SCHEMA, a) for a in CALLS]}")

EXECUTED = [0, 2, 4]
print()
print(f"written call sites {len(CALLS)}, executed {len(EXECUTED)} | "
      f"checker saw {len(CALLS)}, caught {len(found)} | "
      f"schema reached {len(EXECUTED)}, caught "
      f"{len(schema_caught & set(EXECUTED))}")
```

```
  call  type violation  domain violation    runtime    checker  domain rule  schema
     0           False             False   no error      False        False   False
     1            True             False   no error       True        False    True
     2            True             False   no error       True        False    True
     3           False              True   no error      False         True    True
     4           False              True   no error      False         True    True

calls 5, violating 4 | caught by: runtime 0, checker 2, domain rule 2, schema 4
checker and domain rule together: 4
schema messages: [[], ['age: type'], ['name: type'], ['age: negative'], ['name: empty']]

written call sites 5, executed 3 | checker saw 5, caught 2 | schema reached 3, caught 2
```

#### Three Layers, Three Separate Catches

The table spreads four violations across three columns, and no column is a
copy of another.

**Runtime catches 0.** All five lines say "no error." The annotation does not
stop the call; the number measured in the course's first lesson comes out the
same here too.

**The checker catches 2**, and what it catches are the type violations. On the
domain violations, the column says false, because **-5** is an integer, an
empty string is a string.

**The domain rule catches 2**, and what it catches is exactly what the checker
cannot see. The two columns **do not intersect**: what one finds, the other
cannot find.

This has a direct consequence in the row below: **the checker and the domain
rule together catch 4** — all of the violations. Not a sum, a union; because the
two layers' scopes do not overlap, the union comes out equal to the sum. **All
four violations only become visible when the two layers are used together.**

The schema column, by contrast, catches on **4** of the five calls. The reason
is that the schema carries two pieces of information in one place: type and
condition. The message list gives this in detail — `type` on two calls,
`negative` on one, `empty` on one. So the schema does not bring a new ability
to see; it merges what the checker knows and what the domain rule knows into
**the same definition** and applies both at call time.

The message list also exposes one of the schema's design decisions. On the
call whose age is a string, the message is `age: type`; on the one whose age is
negative, it is `age: negative`. Two messages never come out for the same
field, because the test looks at the type first and does not move to the
condition until the type passes. This order is mandatory: the comparison `d >=
0` cannot be performed with a string. A domain condition **assumes the type is
correct**; when writing a rule, knowing where that assumption comes from
matters as much as writing the rule itself.

The wrapping layer has one more difference. When validation does not pass, the
call **never starts at all**; the body does not run, no half-finished work is
left behind. The situation measured in the earlier lessons — "run time only
falls while doing the operation" — disappears here: the fall sits **in front
of** the call.

This has a cost, and it shows up in the measurement: the wrapper applies the
schema from start to finish on **every** call, violation or not. The clean call
gets tested too. The checker, by contrast, does nothing at run time; it does
its job once, before the run. The two layers' costs are of a different kind,
just like their scopes: one, once per run; the other, every time, per call.

#### Scope: Written Code and Reached Call

The last line shows the schema's cost, and this cost is the course's measure's
final word.

Written call sites: **5**, executed in a single run: **3**. The checker sees
all five of the five and declares **2** violations — without ever running the
code. The schema only tests the **3** calls it reaches, and catches **2**
violations in those three; the violations on the two calls it does not reach
are invisible in that run.

The pattern is this: **the checker's scope is the written code, validation's
scope is the reached call.** Writing the schema more carefully does not close
this gap, because the gap is not in carefulness, it is in when the layer runs.
In the same way, writing the checker more carefully does not make it find
domain violations either — that too is a knowledge limit.

The two layers do not substitute for each other, and they do not confirm each
other either: one knows **a little, everywhere**; the other knows **a lot, in
some places**.

The scope difference also decides where validation gets placed. Putting the
schema in front of every function grows the test count with the call count,
and the same value gets tested over and over in the same run. Placing a single
validation, by contrast, at the point where the data **enters** the program
produces a guarantee for every call after that point — the test is done once,
the result is carried inside. This is why the schema in the measurement also
carries the field name: when a violation is declared, which field is at fault
has to be readable far from where the test was performed too.

This is the scaled-up form of what the `Age` class in the previous lesson did.
There, the rule was written into a single constructor's body, and a separate
class was needed for every field; here, the rules are gathered into a single
definition, and the validator reads that definition. What changes is not
**when** the rule runs — both are run time — it is **where the rule is
written**.

#### Summary

- A schema gathers a field's name, expected type, and domain condition into a
  single definition; runtime validation applies that definition at call time,
  and if validation does not pass, the call never starts.
- Three layers catch three separate things: runtime **0**, the checker **2**,
  the domain rule **2**. The two layers' scopes do not intersect, so their
  union is **4** — all four violations only become visible together.
- Schema-based validation alone catches **4**, because it merges what the
  checker and the domain rule know into the same definition; it adds no new
  ability to see.
- The scopes are separate: the checker sees all **5** of the five written call
  sites, the schema tests the **3** calls reached in a single run and catches
  **2** violations in those three.
- The layers' limits are of different kinds; the checker's is a knowledge
  limit, validation's is a scope limit, and neither closes the other by being
  more careful.

#### Course Wrap-Up

The course opened with a single question and measured that same question
across eleven lessons in two halves: **who answered the call, who checked the
claim?** In the first half, the answer was a class; in the second, a layer. In
both, what was measured was not what the design did, it was **where the
responsibility stood**.

| Lesson | Measured | The answerer or catcher |
|---|---|---|
| Class and Instance | whether an attribute sits on the class or the instance: **1** against **3** objects | the namespace that finds the name first — instance dictionary first, then the class |
| Inheritance and Method Resolution Order | where the body of four capabilities is written: **0/4** against **4/4** | the first class in resolution order; `Wrapper` in composition |
| Encapsulation Conventions | how many of four access forms give the value: **3**, blocked **0** | the method body — not the name |
| Special Methods | syntax forms a class writing no special method takes part in: **5**, answers: **0** | the silent default; in a class that writes it, the method itself |
| Properties and Descriptors | the body the same seven accesses run: **0** for a direct field, **7** for an intervening definition | the intervening body; a descriptor is created once on the class |
| Data Classes | special methods a short definition writes: **4**; with options **8**, **6**, and **10** | the methods the short definition writes into the class dictionary |
| Abstract Base Classes and Protocols | of three candidates, **1** passes the lineage test, **3** pass the method test | both tests; but passing does not say the call will be answered |
| Type Hints | **4** of five calls are violations, erroring at runtime **0** | no layer |
| Generics and Type Variables | violations four notations declare: **0**, **4**, **3**, **2** | the side reading the annotation — if a bond is built |
| Static Type Checking | the checker finds **2**, cannot see **2** | the checker; its scope is the written code |
| Runtime Validation | runtime **0**, checker **2**, domain rule **2** | schema-based validation — **4** on the calls it reaches |

The table's two halves give the same pattern. In inheritance, reading where a
body is written did not say where the answer would come from; an annotation
being written into the code did not say the claim would be checked either. In
both cases, the answer to the question does not sit in the writing itself, it
sits in **the mechanism that resolves it**: resolution order in one, a
checking layer in the other.

All these measurements shared an assumption that was never written down: **a
single thread.** When the class giving the answer was asked, a single
resolution chain ran; when the layer catching the violation was asked, calls
ran in sequence, one finishing before the next began. This is why the question
"who answered" always had a single answer.

When there is more than one thread, this question reopens. When the same
object is reached from two places at once, which write left the attribute,
whether a value that passed validation is still valid while the body runs, and
which of the two threads acted first can no longer be read by looking at the
source. The next course, Concurrency and Performance, starts from exactly this
point: in a single thread, who answered is certain; in more than one, this
question gets asked again, and this time its answer depends on an ordering.

