Classes
Make a class with __init__, self, and methods. Build a tiny Agent with goal, step, and run. Know when a function is enough.
A class is a plan for an object. An instance is one object made from that plan. A method is a function that lives on the class. You already use classes. str, list, and dict are types. Now you write your own.
An agent is a natural object. It has a goal. It has a step count. It has a log. It can run. You can store the same facts in a dict. A class helps when those facts travel together and you have actions: run, reset, record.
A class is a plan. An instance holds goal, step, and log. run() is a method on that object.
Data on the object, plus actionsClasses are not required to write an agent. Many good agents are functions plus a state dict. Use a class when the object is real in your head: this agent, that counter, this tool runner.
class, __init__, and self
| Piece | Role |
|---|---|
class Agent | The plan |
__init__ | Runs when you make one object |
self | This object |
self.goal | Data on this object |
run | A method you can call |
self is the first parameter of every method. Python fills it in. You do not pass it at the call site. agent.run(3) is Agent.run(agent, 3) underneath. If you forget self in the definition, the first argument you pass is swallowed as self and the error is confusing.
class Counter:
def __init__(self, start):
self.n = start
def inc(self):
self.n += 1
return self.n
c = Counter(0)
print(c.inc())
print(c.n)Counter(0)calls__init__self.nbelongs to this counter- Two counters do not share
n
Class names use CapitalizedWord: Agent, Counter, ToolCall. That is the usual style. The instance is snake_case: agent, counter.
__init__ should store data and maybe validate. It should not start a paid loop. Call run separately. Tests then build an Agent without spending tokens.
Methods
A method can read and change self. Prefer methods that return a value you can test. A method that only prints is hard to check later. run should return the log or a result dict.
Keep methods small. run can call a smaller method. Do not put the whole program in one method. record(self, row) that appends to self.log is easier to test than a 200-line run.
A method can take extra arguments after self: def run(self, max_steps):. Defaults work: def run(self, max_steps=8):. List defaults are still a bug. Do not write def run(self, log=[]):.
A method should not need ten arguments if those values already live on self. Pass max_steps because it varies per run. Do not pass goal into run if self.goal already holds it. Duplicate sources of truth get out of sync. Read from self. Return something tests can see.
A tiny Agent
Here is a small agent. It stores a goal (what to do), a step (how far it has gone), and a log (what happened). run loops until it hits a max.
class Agent:
def __init__(self, goal):
self.goal = goal
self.step = 0
self.log = []
def run(self, max_steps):
while self.step < max_steps:
self.step += 1
self.log.append({class="tok-s">"step": self.step, class="tok-s">"goal": self.goal})
return self.logThat is enough to see the shape. Later lessons add tools and model calls. The class is still: data on self, plus a few methods.
Two objects must not share one log list by accident. Put self.log = [] in __init__. Do not put a list on the class body:
class Bad:
log = [] class="tok-c"># shared by every Bad instanceThat is the mutability lesson on a class. Every instance would append to one list. The same bug happens if you write def __init__(self, log=[]): and skip passing log. Defaults lesson, again.
| Where the list lives | Shared? | Safe for two agents? |
|---|---|---|
self.log = [] in __init__ | no | yes |
log = [] on the class body | yes | no |
def __init__(self, log=[]) | yes, if callers skip log | no |
self.log = given_list | yes, if two agents get the same list | only if each caller passes a new list |
Walkthrough: leftover state and reset
run mutates self.step and self.log. Call run twice on the same instance and the second call continues from the old step unless you reset. Tests should build a new instance, or you add reset:
def reset(self):
self.step = 0
self.log = []A new empty list, not self.log.clear(), if any caller still holds the old log. clear() mutates the shared object. A new [] isolates. Same copy-vs-change rule.
Validate in __init__ when a bad goal should not create an object: empty goal string, missing tools dict. Raise ValueError with a clear message. Do not start run from __init__. Construction and execution are different moments. Tests need the first without the second.
When not to use a class
A function is enough when you have one job and little saved state.
def done(step, max_steps):
return step >= max_stepsDo not wrap that in a class. Extra classes make the file longer and the idea harder.
Use a class when several values travel together and you have actions on them: goal, step, log, plus run.
| You have | Pick |
|---|---|
| One job, little saved state | A function |
| Several values plus actions | A class |
| Only a pile of fields, almost no behavior | A dataclass (next lesson) |
Dataclasses come later
Typing __init__ by hand gets old when the class is mostly data. The next lesson shows dataclasses. They write some of this for you. Learn class, self, and methods first. Then use the short form when it fits.
What goes wrong
- Forgetting
self. - Shared list on the class body.
- Doing all the work in
__init__. - A class with one method that is really a function.
- Mutating another instance’s log because you copied the name, not the list.
- Calling
runagain in a test and not noticingstepcontinued. - A method that only prints, so tests have nothing to assert.
Run to execute this in your browser. Nothing is sent to a server.
Make two agents. Confirm their logs are different lists. Then call run again on the first agent: step continues from 3 unless you reset. That leftover state is why tests should build a new instance. agent.log is other.log should be False.
How agents use this
You can store agent state in a dict. A class helps when that state grows: goal, step, log, budget. Keep methods small. run should be a short loop. If a job is one function with two arguments, do not make a class. Save classes for objects that hold data and act on it.
A tool runner can be a class with a registry on self.tools and a method call(self, name, **kwargs). Two runners then have two registries. That is how you stub tools in tests: pass a fake dict into __init__.
Frameworks hide this behind graphs and nodes. Underneath there is still an object or a dict with a loop. If you cannot point to self.step or state["step"], you cannot cap the budget. The class is a naming device for that state.
record as its own method is the test seam: tests call record with a fake observation and skip the model. run only loops. If run also parses JSON, calls HTTP, and writes files, you cannot test the budget without a network. Split methods the way you split files.
Two Agent instances in one process is the multi-user case. Each needs its own self.log. Sharing self.tools (a dict of functions) is fine. Sharing self.log is a leak. Put mutable user data on self in __init__ as a new list or dict. Put shared code on the class as methods.
Check your understanding