14.3 Classes and objects
- Approved by
- Panaversity ·
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.
| Part | What it is | In InvoiceBook |
|---|---|---|
| Class | The blueprint | InvoiceBook |
| Object | One thing built from it | new InvoiceBook(invoices, vendors) |
| Fields | Its data | invoices, vendors |
| Constructor | Fills in the fields | Takes the invoices and a VendorDirectory |
| Methods | What it can do | open(), 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 V099V099 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 V099Hold 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')
FalseWhen an agent's test compares two records with == or ===, check which kind of equal it means.

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.
| TypeScript | Python | |
|---|---|---|
| Build an object | new InvoiceBook(...) | InvoiceBook(...), with no new |
| The constructor | constructor(...) | __init__(self, ...) |
| The object itself | this, implicit | self, 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 class | A 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 modeThe 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. 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?
Sources
- Owner
- Panaversity