Skip to content
The AI Agent Factory

14.3 Classes and objects

Approved by
Panaversity ·
13 min read

In everyday life. A builder works from a blueprint for a house. The blueprint is not a house. Each house built from it is one real thing, with its own address and its own paint color. All of them have the same rooms in the same places.

What you will learn. How to read code built from blueprints, by the data each one holds and the job of each of its functions. Also, how the two languages decide that two things are equal.

Why it matters. Much of the code an agent writes, and most code a company publishes for its service, is built this way.

What is a class, and what is an object?

A class is a blueprint for a kind of thing in a program. An object is one thing built from it, also called an instance. In TypeScript, a value in braces, like 13.6's dictionaries, is an object too, made without a class. A class has fields, the data each object holds, and methods, functions that belong to it and work on that object's data. The constructor is the special method that runs when an object is built and fills in its fields. Inside a method, the object refers to itself as this in TypeScript and self in Python.

PartWhat it isIn InvoiceBook
ClassThe blueprintInvoiceBook
ObjectOne thing built from itnew InvoiceBook(invoices, vendors)
FieldsIts datainvoices, vendors
ConstructorFills in the fieldsTakes the invoices and a VendorDirectory
MethodsWhat it can doopen(), openTotalCents(), dueDateOf(inv), dueBy(cutoff)

Data classes. Many classes only hold data, with no real methods: one invoice, one vendor. Python has a short way to write them, @dataclass, which writes the constructor for you. In TypeScript, a plain record type, as in 14.2, does the same job.

How do I read a class I did not write?

By its fields, and by the one-sentence job of each method.

At work. The agent reorganized the scripts from Chapter 12 and Chapter 13 into two small classes, without changing what they do. VendorDirectory looks up names by id. InvoiceBook holds the invoices and a directory, and answers questions about them. Ana reads each method and says what it does in one sentence. When she reaches dueBy, she checks that it calls open(), so paid invoices stay out, and dueDateOf, so empty due dates are computed from the terms. Then the agent adds openTotalCents() for her, test first, from cases she decides.

In TypeScript. An excerpt from src/vendor.ts, then a short run. hockingValley is vendor V005, Hocking Valley Pallets. #byId is a private field: only the class's own methods can see it, and code outside the class that names it is a SyntaxError, so none of the file runs. A Map is a dictionary, like the coat check of 13.6. set stores each vendor under its id, and get finds it. Predict the two lines printed.

export class VendorDirectory {
  #byId = new Map<string, Vendor>();
  constructor(vendors: Vendor[]) {
    for (const v of vendors) this.#byId.set(v.id, v);
  }
  name(id: string): string {
    return this.#byId.get(id)?.name ?? `Unknown vendor ${id}`;
  }
}

const directory = new VendorDirectory([hockingValley]);
console.log(directory.name("V005"));
console.log(directory.name("V099"));
Hocking Valley Pallets
Unknown vendor V099

V099 is not in the directory, so get finds nothing. ?. and ?? mean "if the vendor is missing, use the text after ?? instead".

In Python. An excerpt from vendor.py, then the same run. __init__ is the constructor, and every method takes self first. {v.id: v for v in vendors} builds a dictionary with one entry for each vendor, under its id. vendor.name if vendor else ... gives the name when the vendor was found, and the text after else when it was not.

class VendorDirectory:
    def __init__(self, vendors: list[Vendor]) -> None:
        self._by_id = {v.id: v for v in vendors}

    def name(self, vendor_id: str) -> str:
        vendor = self._by_id.get(vendor_id)
        return vendor.name if vendor else f"Unknown vendor {vendor_id}"

directory = VendorDirectory([hocking_valley])
print(directory.name("V005"))
print(directory.name("V099"))
Hocking Valley Pallets
Unknown vendor V099

Hold another object, or extend it?

Usually hold it. A class can be built two ways from another class. With inheritance, a class extends another and becomes a kind of it, taking all its methods. With composition, a class holds another object as a field and uses it. An InvoiceBook is not a kind of vendor directory. It has one. Prefer composition: each class stays small, you can test each one alone, and a change to one does not change the other by accident. Agent code sometimes reaches for inheritance first. Ask whether "is a kind of" is really true.

Why are two objects with the same data not equal?

Because the language asks whether they are the same object, unless a method compares their fields. Two houses with the same paint are still two houses, at two addresses.

Methods Python calls for you. Python names some methods with two underscores on each side, such as __init__. They are called dunder methods, for "double underscore". Your code rarely calls them. Python calls __init__ when an object is built, __repr__ when it is printed, and __eq__ when it is compared with ==. @dataclass writes all three for you. A plain class without __eq__ compares by identity, so two objects with the same data are not equal.

TypeScript has no dunder methods. === on two objects always asks whether they are the same object, so code compares a field, such as the vendor id. Predict each true and false.

In TypeScript.

type Vendor = { id: string; name: string };
const a: Vendor = { id: "V005", name: "Hocking Valley Pallets" };
const b: Vendor = { id: "V005", name: "Hocking Valley Pallets" };
console.log(a === b, a.id === b.id);
console.log(a);
false true
{ id: 'V005', name: 'Hocking Valley Pallets' }

In Python.

from dataclasses import dataclass

@dataclass
class Vendor:
    id: str
    name: str

class PlainVendor:
    def __init__(self, id: str, name: str) -> None:
        self.id = id
        self.name = name

a: Vendor = Vendor("V005", "Hocking Valley Pallets")
b: Vendor = Vendor("V005", "Hocking Valley Pallets")
print(a == b, a)
print(PlainVendor("V005", "Hocking Valley Pallets") == PlainVendor("V005", "Hocking Valley Pallets"))
True Vendor(id='V005', name='Hocking Valley Pallets')
False

When an agent's test compares two records with == or ===, check which kind of equal it means.

Figure 14.3. Three panels, each with two separate objects a and b holding id V005 and the name Hocking Valley Pallets. In TypeScript, a === b is false, because it compares identity. A Python plain class gives a == b False, because it has no eq of its own. A Python @dataclass gives a == b True, because its generated eq compares the fields. To ask whether it is the same vendor, compare the ids.

Figure 14.3. Same data, three answers. Only an eq method, which @dataclass writes, makes two objects with the same data equal.

Where the languages differ.

TypeScriptPython
Build an objectnew InvoiceBook(...)InvoiceBook(...), with no new
The constructorconstructor(...)__init__(self, ...)
The object itselfthis, implicitself, written as the first parameter
A private field#byId, enforced_by_id, a naming habit, not enforced when the code runs; strict Pyright reports use from outside the class
A data-only classA plain record type@dataclass
Two objects with the same data=== is false. Compare a field== is True with @dataclass, False for a plain class

Which TypeScript will Node refuse?

A few shortcuts. Node runs TypeScript by removing the type labels, so it refuses anything it cannot simply remove: enum, a named list of fixed values, namespace with code inside it, and a constructor parameter property, a shortcut that declares a field inside the constructor's parameter list. Brightline's tsconfig.json sets erasableSyntaxOnly, so the type checker refuses them first. If an agent writes one, ask for the plain form. For a parameter property, declare the field, then set it in the constructor. For an enum, use a literal type, as in 14.2.

This shortcut, common in agent-written TypeScript, does not run. "Strip-only" is Node's name for removing the labels:

constructor(readonly names: string[]) {}
SyntaxError [ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX]: TypeScript parameter property is not supported in strip-only mode

The operating rule: read a class by its fields and the one-sentence job of each method. Prefer an object that holds another to one that extends it, and use only TypeScript that Node can run by erasing the types.

Figure 14.4. An InvoiceBook holding its invoices and a VendorDirectory. Its methods are open, openTotalCents, dueDateOf and dueBy, and dueBy calls the directory's name(id) method to add vendor names. A side panel contrasts has a, which is composition, with is a kind of, which is inheritance: an invoice book is not a vendor directory.

Figure 14.4. Composition. The book has a directory and asks it for names. It is not a kind of directory.

Practice it. Practice: classes and objects, in your own tool, ten minutes.

Check yourself

Question 1 / 8

0 answered

An agent's test builds two invoices from the same row and compares them.class Invoice: def __init__(self, number: str, amount_cents: int) -> None: self.number = number self.amount_cents = amount_cents a: Invoice = Invoice("INV-5318", 64000) b: Invoice = Invoice("INV-5318", 64000) print(a == b, a.number == b.number)What does it print?

Owner
Panaversity