Skip to content
aviral gupta

// A1.6 · ~40 min · Advanced

Build: attribute hooks, class creation, Vector and Matrix

After this lesson you can hook into failed and all attribute lookups, run code whenever a subclass is created, recognise when a metaclass is the right tool, and you have built a Vector and Matrix pair with operators.

Lesson 6 of 6 in A1 The data model

End of the module

You will be able to

  • Choose between __getattr__ and __getattribute__ and write them without recursion
  • Use __init_subclass__, with class keywords, to check or register subclasses
  • Explain what a metaclass is and when it runs, compared with __init_subclass__
  1. Warm-up · Activity 1 of 7

    Warm-up from lesson A1.5: an instance dictionary entry has the same name as a non-data descriptor in the class. Which one does obj.name return?

  2. Predict · Activity 2 of 7

    Predict before you read on: the class defines __getattr__. What does this print?

    class Fallback:
        color = "red"
    
        def __getattr__(self, name):
            return f"<{name} missing>"
    
    
    f = Fallback()
    print(f.color, f.size)
  3. Practice · Activity 3 of 7

    Fill in the hook so that missing attributes give "default" while existing ones keep their value.

    class Defaults:
        known = "set"
    
        def ____(self, name):
            return "default"
    def (self, name):
  4. Practice · Activity 4 of 7

    Counter overrides __getattribute__. What does this print?

    class Counter:
        def __init__(self):
            self.value = 1
    
        def __getattribute__(self, name):
            print("get", name, end="; ")
            return object.__getattribute__(self, name)
    
    
    c = Counter()
    print(c.value)
  5. Practice · Activity 5 of 7

    Match each hook to when Python calls it.

  6. Brain teaser · Activity 6 of 7

    Brain teaser. B subclasses A, which subclasses Base. What does this print?

    class Base:
        count = 0
    
        def __init_subclass__(cls, **kwargs):
            super().__init_subclass__(**kwargs)
            Base.count += 1
    
    
    class A(Base):
        pass
    
    
    class B(A):
        pass
    
    
    print(Base.count)
  7. Apply · Activity 7 of 7

    Mini-task. Write a base class Shape that checks each subclass as soon as its class statement runs: a subclass without an area method raises TypeError("<Name> must define area()"), and every accepted subclass name is added to Shape.registry.

    Check your work against this list

Build it yourself

Read the worked example, then write the exercises. Your code runs in your browser or on your computer and is never uploaded.

Worked example

Records with fields from the class line

Record uses two hooks. __init_subclass__ reads the fields keyword from each class line and registers the subclass. __getattr__ turns the stored values into attributes, and only runs when normal lookup fails, so book.fields still comes from the class. Try adding a Movie subclass, or reading book._data before __init__ has run.

main.py

class Record:
    """Base class: each subclass names its fields in the class line."""

    fields: tuple[str, ...] = ()
    kinds: dict[str, type["Record"]] = {}

    def __init_subclass__(cls, /, fields: str = "", **kwargs: object) -> None:
        super().__init_subclass__(**kwargs)
        cls.fields = tuple(fields.split())
        Record.kinds[cls.__name__.lower()] = cls

    def __init__(self, *values: object) -> None:
        if len(values) != len(self.fields):
            raise TypeError(f"{type(self).__name__} needs {len(self.fields)} values")
        self._data = dict(zip(self.fields, values))

    def __getattr__(self, name: str) -> object:
        # Only called when the normal lookup has failed.
        if name.startswith("_"):
            raise AttributeError(name)  # never look up _data through itself
        try:
            return self._data[name]
        except KeyError:
            raise AttributeError(f"{type(self).__name__!r} has no field {name!r}") from None


class Book(Record, fields="title author"):
    pass


class Song(Record, fields="title artist year"):
    pass


print(Record.kinds)
book = Book("Dune", "Herbert")
print(book.title, book.author, book.fields)
try:
    book.year
except AttributeError as err:
    print(err)
print(type(Book).__name__)

Run it with

python main.py

Output

{'book': <class '__main__.Book'>, 'song': <class '__main__.Song'>}
Dune Herbert ('title', 'author')
'Book' has no field 'year'
type
  • fields="title author" is a class keyword; Python hands it to Record.__init_subclass__.
  • The name check for "_" stops __getattr__ from looking up self._data through itself when _data does not exist yet.
  • The KeyError is turned into AttributeError, so hasattr(book, "year") gives False.
  • type(Book) is type: this needed no metaclass at all.
Change it and run it

Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.

The first run downloads Python for your browser (up to 6.5 MB) and keeps it cached. Your code stays on your device.

Exercises

Exercise 1 of 3

Step 1: a Vector container with named components

Module build, step 1 of 3. Give Vector the container protocol from A1.1 and A1.3: __repr__ as Vector(3, 4), __len__, __iter__, __getitem__, __eq__ and a matching __hash__. Then add __getattr__ so that v.x, v.y and v.z read components 0 to 2 when they exist; every other missing name raises AttributeError.

Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.

The first run downloads Python for your browser (up to 6.5 MB) and keeps it cached. Your code stays on your device.

Hints
  1. Hint 1

    Build the repr with ", ".join(map(repr, self._components)) so that one component gives Vector(1.5).

  2. Hint 2

    Hash the same tuple that __eq__ compares: hash(self._components).

  3. Hint 3

    In __getattr__, index = self._names.find(name); accept it only if len(name) == 1 and 0 <= index < len(self._components). Check len(name) first: that way a missing _components never recurses.

Show a solution

One way to solve it. Yours can look different and still pass the checks.

from collections.abc import Iterator


class Vector:
    """An immutable vector of numbers."""

    _names = "xyz"

    def __init__(self, *components: float) -> None:
        self._components = tuple(components)

    def __repr__(self) -> str:
        return f"Vector({', '.join(map(repr, self._components))})"

    def __len__(self) -> int:
        return len(self._components)

    def __iter__(self) -> Iterator[float]:
        return iter(self._components)

    def __getitem__(self, index: int) -> float:
        return self._components[index]

    def __eq__(self, other: object) -> bool:
        if not isinstance(other, Vector):
            return NotImplemented
        return self._components == other._components

    def __hash__(self) -> int:
        return hash(self._components)

    def __getattr__(self, name: str) -> float:
        index = self._names.find(name)
        if len(name) == 1 and 0 <= index < len(self._components):
            return self._components[index]
        raise AttributeError(f"{type(self).__name__!r} object has no attribute {name!r}")


if __name__ == "__main__":
    v = Vector(3, 4)
    print(v, len(v), list(v), v[1])
    print(v.x, v.y, v == Vector(3, 4), {v, Vector(3, 4)})
    try:
        v.z
    except AttributeError as err:
        print(err)
Run it on your computer

Install Python 3.14 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.

main.py

from collections.abc import Iterator


class Vector:
    """An immutable vector of numbers."""

    _names = "xyz"

    def __init__(self, *components: float) -> None:
        self._components = tuple(components)

    # Step 1 of the build. Add:
    # __repr__  -> "Vector(3, 4)"
    # __len__, __iter__ and __getitem__ over the components
    # __eq__ (NotImplemented for non-vectors) and a matching __hash__
    # __getattr__: v.x, v.y, v.z give components 0, 1, 2 if the vector
    #   has that many; any other name raises AttributeError.


if __name__ == "__main__":
    v = Vector(3, 4)
    print(v)

test_main.py

from main import Vector


def test_repr():
    """repr shows the components"""
    assert repr(Vector(3, 4)) == "Vector(3, 4)", f"repr is {Vector(3, 4)!r}"
    assert repr(Vector(1.5)) == "Vector(1.5)", f"repr of a one-component vector is {Vector(1.5)!r}"


def test_sequence_protocol():
    """len, iteration and indexing work"""
    v = Vector(3, 4, 5)
    got = (len(v), list(v), v[-1])
    assert got == (3, [3, 4, 5], 5), f"(len(v), list(v), v[-1]) is {got!r}"


def test_equality_and_hash():
    """Equal vectors compare equal and hash alike"""
    assert Vector(1, 2) == Vector(1, 2), "Vector(1, 2) != Vector(1, 2)"
    assert Vector(1, 2) != Vector(2, 1), "Vector(1, 2) == Vector(2, 1)"
    assert len({Vector(1, 2), Vector(1, 2)}) == 1, "equal vectors must have equal hashes"
    assert Vector(1, 2) != (1, 2), "a Vector should not equal a tuple"


def test_named_components():
    """x, y and z read the first three components"""
    v = Vector(3, 4, 5)
    got = (v.x, v.y, v.z)
    assert got == (3, 4, 5), f"(v.x, v.y, v.z) is {got!r}"


def test_missing_names_raise():
    """z on a 2D vector and unknown names raise AttributeError"""
    for name in ["z", "w", "xy"]:
        try:
            getattr(Vector(3, 4), name)
        except AttributeError:
            continue
        raise AssertionError(f"Vector(3, 4).{name} should raise AttributeError")

On macOS and Linux, type python3 wherever these commands say python, as in the first lesson.

Run the program:

python main.py

Run the checks (needs learnrun.py in the same folder):

python learnrun.py test
Download learnrun.py

Exercise 2 of 3

Step 2: vector operators

Step 2 of 3. Add the operators from A1.2: v + w, v - w and -v component by component, v * 2 and 2 * v, v @ w as the dot product, and abs(v) as the length. + and @ raise ValueError("lengths differ: 2 and 3") for different lengths. Every operator returns NotImplemented for operand types it does not support.

Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.

The first run downloads Python for your browser (up to 6.5 MB) and keeps it cached. Your code stays on your device.

Hints
  1. Hint 1

    A helper _same_length(other) keeps the length check in one place.

  2. Hint 2

    v - w can reuse the others: return self + -other.

  3. Hint 3

    Accept only int and float in __mul__, then write __rmul__ = __mul__. Without the type check, v * "a" would quietly build a vector of strings.

Show a solution

One way to solve it. Yours can look different and still pass the checks.

import math
from collections.abc import Iterator


class Vector:
    """An immutable vector of numbers."""

    _names = "xyz"

    def __init__(self, *components: float) -> None:
        self._components = tuple(components)

    def __repr__(self) -> str:
        return f"Vector({', '.join(map(repr, self._components))})"

    def __len__(self) -> int:
        return len(self._components)

    def __iter__(self) -> Iterator[float]:
        return iter(self._components)

    def __getitem__(self, index: int) -> float:
        return self._components[index]

    def __eq__(self, other: object) -> bool:
        if not isinstance(other, Vector):
            return NotImplemented
        return self._components == other._components

    def __hash__(self) -> int:
        return hash(self._components)

    def __getattr__(self, name: str) -> float:
        index = self._names.find(name)
        if len(name) == 1 and 0 <= index < len(self._components):
            return self._components[index]
        raise AttributeError(f"{type(self).__name__!r} object has no attribute {name!r}")


    def _same_length(self, other: "Vector") -> None:
        if len(self) != len(other):
            raise ValueError(f"lengths differ: {len(self)} and {len(other)}")

    def __add__(self, other: object) -> "Vector":
        if not isinstance(other, Vector):
            return NotImplemented
        self._same_length(other)
        return Vector(*(a + b for a, b in zip(self, other)))

    def __sub__(self, other: object) -> "Vector":
        if not isinstance(other, Vector):
            return NotImplemented
        return self + -other

    def __neg__(self) -> "Vector":
        return Vector(*(-a for a in self))

    def __mul__(self, scalar: object) -> "Vector":
        if not isinstance(scalar, (int, float)):
            return NotImplemented
        return Vector(*(a * scalar for a in self))

    __rmul__ = __mul__

    def __matmul__(self, other: object) -> float:
        if not isinstance(other, Vector):
            return NotImplemented
        self._same_length(other)
        return sum(a * b for a, b in zip(self, other))

    def __abs__(self) -> float:
        return math.hypot(*self)


if __name__ == "__main__":
    v, w = Vector(3, 4), Vector(1, 2)
    print(v + w, v - w, -w)
    print(v * 2, 2 * v, v @ w, abs(v))
    try:
        v + Vector(1, 2, 3)
    except ValueError as err:
        print(err)
Run it on your computer

Install Python 3.14 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.

main.py

import math
from collections.abc import Iterator


class Vector:
    """An immutable vector of numbers."""

    _names = "xyz"

    def __init__(self, *components: float) -> None:
        self._components = tuple(components)

    def __repr__(self) -> str:
        return f"Vector({', '.join(map(repr, self._components))})"

    def __len__(self) -> int:
        return len(self._components)

    def __iter__(self) -> Iterator[float]:
        return iter(self._components)

    def __getitem__(self, index: int) -> float:
        return self._components[index]

    def __eq__(self, other: object) -> bool:
        if not isinstance(other, Vector):
            return NotImplemented
        return self._components == other._components

    def __hash__(self) -> int:
        return hash(self._components)

    def __getattr__(self, name: str) -> float:
        index = self._names.find(name)
        if len(name) == 1 and 0 <= index < len(self._components):
            return self._components[index]
        raise AttributeError(f"{type(self).__name__!r} object has no attribute {name!r}")


    # Step 2 of the build. Add operators; each returns NotImplemented for
    # operands it does not support:
    # v + w, v - w and -v, component by component; + and - raise
    #   ValueError("lengths differ: 2 and 3") for different lengths
    # v * 2 and 2 * v (int or float only)
    # v @ w, the dot product (same length check)
    # abs(v), the length: math.hypot(*v)


if __name__ == "__main__":
    v, w = Vector(3, 4), Vector(1, 2)
    print(v + w, v - w, -w)
    print(v * 2, 2 * v, v @ w, abs(v))
    try:
        v + Vector(1, 2, 3)
    except ValueError as err:
        print(err)

test_main.py

from main import Vector


def test_add_sub_neg():
    """+, - and unary - work component by component"""
    v, w = Vector(3, 4), Vector(1, 2)
    got = (v + w, v - w, -w)
    assert got == (Vector(4, 6), Vector(2, 2), Vector(-1, -2)), f"(v + w, v - w, -w) is {got!r}"


def test_scalar_on_both_sides():
    """v * 2 and 2 * v give the same vector"""
    v = Vector(1, 2)
    got = (v * 2, 2 * v, 0.5 * v)
    assert got == (Vector(2, 4), Vector(2, 4), Vector(0.5, 1.0)), f"(v * 2, 2 * v, 0.5 * v) is {got!r}"


def test_dot_and_abs():
    """@ is the dot product and abs() the length"""
    dot = Vector(1, 2, 3) @ Vector(4, 5, 6)
    assert dot == 32, f"Vector(1, 2, 3) @ Vector(4, 5, 6) is {dot!r}, expected 32"
    assert abs(Vector(3, 4)) == 5.0, f"abs(Vector(3, 4)) is {abs(Vector(3, 4))!r}, expected 5.0"


def test_length_mismatch():
    """Vectors of different lengths raise ValueError"""
    for text, op in [("+", lambda a, b: a + b), ("-", lambda a, b: a - b), ("@", lambda a, b: a @ b)]:
        try:
            op(Vector(1, 2), Vector(1, 2, 3))
        except ValueError:
            continue
        raise AssertionError(f"Vector(1, 2) {text} Vector(1, 2, 3) should raise ValueError")


def test_unsupported_operands():
    """Other operand types end in TypeError through NotImplemented"""
    cases = [
        ("Vector(1, 2) + (1, 2)", lambda: Vector(1, 2) + (1, 2)),
        ('Vector(1, 2) * "a"', lambda: Vector(1, 2) * "a"),
        ('"a" * Vector(1, 2)', lambda: "a" * Vector(1, 2)),
        ("Vector(1, 2) @ 3", lambda: Vector(1, 2) @ 3),
    ]
    for text, op in cases:
        try:
            op()
        except TypeError:
            continue
        raise AssertionError(f"{text} should raise TypeError")

On macOS and Linux, type python3 wherever these commands say python, as in the first lesson.

Run the program:

python main.py

Run the checks (needs learnrun.py in the same folder):

python learnrun.py test
Download learnrun.py

Exercise 3 of 3

Step 3: a Matrix built on Vector

Step 3 of 3. Matrix stores its rows as Vectors and already has shape, T (transpose), indexing with m[i, j], repr and equality. Add m + n (ValueError when the shapes differ) and m @ other: with a Vector it returns the Vector of row dot products, with a Matrix the matrix product. Return NotImplemented for anything else.

Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.

The first run downloads Python for your browser (up to 6.5 MB) and keeps it cached. Your code stays on your device.

Hints
  1. Hint 1

    Rows are Vectors, so row @ other and a + b already do the arithmetic and the length checks.

  2. Hint 2

    Check isinstance(other, Vector) and isinstance(other, Matrix) separately in __matmul__; the result type differs.

  3. Hint 3

    Entry (i, j) of m @ n is row i of m @ column j of n. The columns of n are other.T.rows: Matrix([row @ column for column in columns] for row in self.rows).

Show a solution

One way to solve it. Yours can look different and still pass the checks.

import math
from collections.abc import Iterable, Iterator


class Vector:
    """An immutable vector of numbers."""

    _names = "xyz"

    def __init__(self, *components: float) -> None:
        self._components = tuple(components)

    def __repr__(self) -> str:
        return f"Vector({', '.join(map(repr, self._components))})"

    def __len__(self) -> int:
        return len(self._components)

    def __iter__(self) -> Iterator[float]:
        return iter(self._components)

    def __getitem__(self, index: int) -> float:
        return self._components[index]

    def __eq__(self, other: object) -> bool:
        if not isinstance(other, Vector):
            return NotImplemented
        return self._components == other._components

    def __hash__(self) -> int:
        return hash(self._components)

    def __getattr__(self, name: str) -> float:
        index = self._names.find(name)
        if len(name) == 1 and 0 <= index < len(self._components):
            return self._components[index]
        raise AttributeError(f"{type(self).__name__!r} object has no attribute {name!r}")


    def _same_length(self, other: "Vector") -> None:
        if len(self) != len(other):
            raise ValueError(f"lengths differ: {len(self)} and {len(other)}")

    def __add__(self, other: object) -> "Vector":
        if not isinstance(other, Vector):
            return NotImplemented
        self._same_length(other)
        return Vector(*(a + b for a, b in zip(self, other)))

    def __sub__(self, other: object) -> "Vector":
        if not isinstance(other, Vector):
            return NotImplemented
        return self + -other

    def __neg__(self) -> "Vector":
        return Vector(*(-a for a in self))

    def __mul__(self, scalar: object) -> "Vector":
        if not isinstance(scalar, (int, float)):
            return NotImplemented
        return Vector(*(a * scalar for a in self))

    __rmul__ = __mul__

    def __matmul__(self, other: object) -> float:
        if not isinstance(other, Vector):
            return NotImplemented
        self._same_length(other)
        return sum(a * b for a, b in zip(self, other))

    def __abs__(self) -> float:
        return math.hypot(*self)



class Matrix:
    """An immutable matrix, stored as a tuple of row vectors."""

    def __init__(self, rows: Iterable[Iterable[float]]) -> None:
        self.rows = tuple(Vector(*row) for row in rows)
        if len({len(row) for row in self.rows}) > 1:
            raise ValueError("all rows must have the same length")

    @property
    def shape(self) -> tuple[int, int]:
        return len(self.rows), len(self.rows[0]) if self.rows else 0

    @property
    def T(self) -> "Matrix":
        return Matrix(zip(*self.rows))

    def __repr__(self) -> str:
        return f"Matrix({[list(row) for row in self.rows]})"

    def __getitem__(self, index: tuple[int, int]) -> float:
        i, j = index
        return self.rows[i][j]

    def __eq__(self, other: object) -> bool:
        if not isinstance(other, Matrix):
            return NotImplemented
        return self.rows == other.rows

    def __hash__(self) -> int:
        return hash(self.rows)

    def __add__(self, other: object) -> "Matrix":
        if not isinstance(other, Matrix):
            return NotImplemented
        if self.shape != other.shape:
            raise ValueError(f"shapes differ: {self.shape} and {other.shape}")
        return Matrix(a + b for a, b in zip(self.rows, other.rows))

    def __matmul__(self, other: object) -> "Matrix | Vector":
        if isinstance(other, Vector):
            return Vector(*(row @ other for row in self.rows))
        if isinstance(other, Matrix):
            columns = other.T.rows
            return Matrix([row @ column for column in columns] for row in self.rows)
        return NotImplemented


if __name__ == "__main__":
    m = Matrix([[1, 2], [3, 4]])
    identity = Matrix([[1, 0], [0, 1]])
    print(m.shape, m[1, 0], m.T)
    print(m + identity)
    print(m @ Vector(1, 1))
    print(m @ identity == m, m @ m)
Run it on your computer

Install Python 3.14 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.

main.py

import math
from collections.abc import Iterable, Iterator


class Vector:
    """An immutable vector of numbers."""

    _names = "xyz"

    def __init__(self, *components: float) -> None:
        self._components = tuple(components)

    def __repr__(self) -> str:
        return f"Vector({', '.join(map(repr, self._components))})"

    def __len__(self) -> int:
        return len(self._components)

    def __iter__(self) -> Iterator[float]:
        return iter(self._components)

    def __getitem__(self, index: int) -> float:
        return self._components[index]

    def __eq__(self, other: object) -> bool:
        if not isinstance(other, Vector):
            return NotImplemented
        return self._components == other._components

    def __hash__(self) -> int:
        return hash(self._components)

    def __getattr__(self, name: str) -> float:
        index = self._names.find(name)
        if len(name) == 1 and 0 <= index < len(self._components):
            return self._components[index]
        raise AttributeError(f"{type(self).__name__!r} object has no attribute {name!r}")


    def _same_length(self, other: "Vector") -> None:
        if len(self) != len(other):
            raise ValueError(f"lengths differ: {len(self)} and {len(other)}")

    def __add__(self, other: object) -> "Vector":
        if not isinstance(other, Vector):
            return NotImplemented
        self._same_length(other)
        return Vector(*(a + b for a, b in zip(self, other)))

    def __sub__(self, other: object) -> "Vector":
        if not isinstance(other, Vector):
            return NotImplemented
        return self + -other

    def __neg__(self) -> "Vector":
        return Vector(*(-a for a in self))

    def __mul__(self, scalar: object) -> "Vector":
        if not isinstance(scalar, (int, float)):
            return NotImplemented
        return Vector(*(a * scalar for a in self))

    __rmul__ = __mul__

    def __matmul__(self, other: object) -> float:
        if not isinstance(other, Vector):
            return NotImplemented
        self._same_length(other)
        return sum(a * b for a, b in zip(self, other))

    def __abs__(self) -> float:
        return math.hypot(*self)



class Matrix:
    """An immutable matrix, stored as a tuple of row vectors."""

    def __init__(self, rows: Iterable[Iterable[float]]) -> None:
        self.rows = tuple(Vector(*row) for row in rows)
        if len({len(row) for row in self.rows}) > 1:
            raise ValueError("all rows must have the same length")

    @property
    def shape(self) -> tuple[int, int]:
        return len(self.rows), len(self.rows[0]) if self.rows else 0

    @property
    def T(self) -> "Matrix":
        return Matrix(zip(*self.rows))

    def __repr__(self) -> str:
        return f"Matrix({[list(row) for row in self.rows]})"

    def __getitem__(self, index: tuple[int, int]) -> float:
        i, j = index
        return self.rows[i][j]

    def __eq__(self, other: object) -> bool:
        if not isinstance(other, Matrix):
            return NotImplemented
        return self.rows == other.rows

    def __hash__(self) -> int:
        return hash(self.rows)

    # Step 3 of the build. Add, returning NotImplemented for other types:
    # m + n, row by row; ValueError("shapes differ: (2, 2) and (2, 3)")
    #   when the shapes differ
    # m @ v with a Vector: a Vector of the dot products row @ v
    # m @ n with a Matrix: a Matrix whose entry (i, j) is row i of m
    #   @ column j of n (the columns of n are the rows of n.T)


if __name__ == "__main__":
    m = Matrix([[1, 2], [3, 4]])
    identity = Matrix([[1, 0], [0, 1]])
    print(m.shape, m[1, 0], m.T)
    print(m + identity)
    print(m @ Vector(1, 1))
    print(m @ identity == m, m @ m)

test_main.py

from main import Matrix, Vector


def test_add():
    """m + n adds entry by entry"""
    got = Matrix([[1, 2], [3, 4]]) + Matrix([[1, 0], [0, 1]])
    assert got == Matrix([[2, 2], [3, 5]]), f"the sum is {got!r}"


def test_add_shape_mismatch():
    """Adding matrices of different shapes raises ValueError"""
    try:
        Matrix([[1, 2]]) + Matrix([[1], [2]])
    except ValueError:
        return
    raise AssertionError("a (1, 2) plus a (2, 1) matrix should raise ValueError")


def test_matrix_times_vector():
    """m @ v gives a Vector"""
    got = Matrix([[1, 2], [3, 4]]) @ Vector(1, 1)
    assert isinstance(got, Vector) and got == Vector(3, 7), f"m @ Vector(1, 1) is {got!r}, expected Vector(3, 7)"


def test_matrix_times_matrix():
    """m @ n is the matrix product, also for non-square shapes"""
    a = Matrix([[1, 2, 3], [4, 5, 6]])
    b = Matrix([[7, 8], [9, 10], [11, 12]])
    got = a @ b
    assert got == Matrix([[58, 64], [139, 154]]), f"a @ b is {got!r}"


def test_unsupported_operands():
    """Numbers are not supported and end in TypeError"""
    m = Matrix([[1, 2], [3, 4]])
    for text, op in [("m @ 2", lambda: m @ 2), ("m + 1", lambda: m + 1)]:
        try:
            op()
        except TypeError:
            continue
        raise AssertionError(f"{text} should raise TypeError")

On macOS and Linux, type python3 wherever these commands say python, as in the first lesson.

Run the program:

python main.py

Run the checks (needs learnrun.py in the same folder):

python learnrun.py test
Download learnrun.py

Common mistakes

Reading self.attr inside __getattribute__

class Logged:
    def __init__(self):
        self.count = 0

    def __getattribute__(self, name):
        self.count += 1
        return object.__getattribute__(self, name)


print(Logged().count)

What Python prints

RecursionError: maximum recursion depth exceeded

Why, and the fix

self.count inside __getattribute__ is itself an attribute read, so the method calls itself forever. Read and write your own attributes through object.__getattribute__(self, "count") and object.__setattr__, or use __getattr__ if you only need the failed lookups.

A class keyword that __init_subclass__ does not accept

class Plugin:
    registry = {}

    def __init_subclass__(cls):
        super().__init_subclass__()
        Plugin.registry[cls.__name__] = cls


class Csv(Plugin, name="csv"):
    pass

What Python prints

TypeError: Plugin.__init_subclass__() got an unexpected keyword argument 'name'

Why, and the fix

Keywords in the class line are passed to the parent's __init_subclass__. Declare the ones you use and forward the rest: def __init_subclass__(cls, /, name, **kwargs): super().__init_subclass__(**kwargs).

Mixing two unrelated metaclasses

class MetaA(type):
    pass


class MetaB(type):
    pass


class A(metaclass=MetaA):
    pass


class B(metaclass=MetaB):
    pass


class C(A, B):
    pass

What Python prints

TypeError: metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases

Why, and the fix

C needs one metaclass that is a subclass of both MetaA and MetaB, and none exists. Define class MetaAB(MetaA, MetaB) and use it, or better, replace the metaclasses with __init_subclass__, which combines through normal inheritance.

Python in the browser: Pyodide 314.0.7, MPL-2.0. Licence and source

Exit ticket

5 questions, no hints. Score 80% or more to complete the lesson.

Finish every activity above to unlock the exit ticket.

Report a problem

Spotted something wrong or unclear? Say what, and it will be checked and fixed.

#

At least 20 characters.

Only if you want a reply.

Key ideas

__getattr__ is a fallback, __getattribute__ is the front door

obj.name first runs __getattribute__, which does the whole lookup: data descriptors, instance dictionary, class. Only if that raises AttributeError does Python call __getattr__(name). So __getattr__ is cheap and safe for defaults, proxies or computed names; it must raise AttributeError for names it does not handle, or hasattr() and getattr() with a default break. Overriding __getattribute__ intercepts every access, including self.anything inside it: call object.__getattribute__(self, name) to avoid endless recursion.

__init_subclass__ runs for each new subclass

When a class statement creates a subclass, Python calls __init_subclass__ on the parent, with the new class as cls. It is implicitly a classmethod and is not called for the class that defines it. Keywords in the class line, as in class Csv(Plugin, name="csv"), are passed to it; take the ones you need and pass the rest to super().__init_subclass__(**kwargs). Typical uses: registries, checks, and defaults per subclass.

Metaclasses create classes

Classes are objects too, and their class is their metaclass, normally type. class Base(metaclass=Meta) makes Python call Meta(name, bases, namespace) to build Base, and subclasses inherit the metaclass. Meta.__new__ or __init__ can then change every class in the hierarchy, including Base itself. That is powerful but heavy: two bases with unrelated metaclasses cannot be combined. Prefer __init_subclass__, a class decorator or descriptors when they do the job.

Sources

Last reviewed September 29, 2026