# 14.3 Classes and objects (/building-ai-workers/structuring-programs/classes-and-objects)

---
type: Document
title: "14.3 Classes and objects"
description: "How to read a class by the data it holds and the job of each method, when to hold another object instead of extending it, and how TypeScript and Python decide that two objects are equal."
status: stable
order: 314.03
ksor:
  owner: team:panaversity
  audience: [ public ]
  approval:
    by: process:panaversity
    at: 2026-11-02T07:46:23Z
chapter: "14"
part: III
expert_status: builder-only
concepts: [ "14.3" ]
last_verified: 2026-10-30
sources:
  - id: node-typescript
    title: "Modules: TypeScript (Node.js, accessed 2026-10-30)"
    resource: https://nodejs.org/api/typescript.html
generated:
  at: 2026-11-02T07:46:23Z
  by: process:panaversity
trust_tier: unverified
build_id: sha256:e1fbcb7d9a8f8b9b7cb96875022325c02c9ba4be94cef13b301cbd39a9edad85
dirty: true
ksor_version: 0.0.60
---

**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](types-you-write-down.md), 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](../agentic-coding/overview.md) and [Chapter 13](../programming-fundamentals/overview.md) 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](../programming-fundamentals/lists-and-dictionaries.md). `set` stores each vendor under its id, and `get` finds it. Predict the two lines printed.

```ts
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"));
```

```text
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.

```python
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"))
```

```text
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.**

```ts
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);
```

```text
false true
{ id: 'V005', name: 'Hocking Valley Pallets' }
```

**In Python.**

```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"))
```

```text
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.](img/equality.png)

*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](types-you-write-down.md).

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

```text
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.](img/composition.png)

*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](both-ai-vendors.md#practice-classes-and-objects), in your own tool, ten minutes.
