JJoeven

Curriculum/Python

Classes

Make a class with __init__, self, and methods. Build a tiny Agent with goal, step, and run. Know when a function is enough.

intermediate21 min25 / 37

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.

Data on the object, plus actions
goalsteplogrun()

A class is a plan. An instance holds goal, step, and log. run() is a method on that object.

Data on the object, plus actions

Classes 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

PieceRole
class AgentThe plan
__init__Runs when you make one object
selfThis object
self.goalData on this object
runA 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.

python
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.n belongs 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.

python
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.log

That 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:

python
class Bad:
    log = []  class="tok-c"># shared by every Bad instance

That 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 livesShared?Safe for two agents?
self.log = [] in __init__noyes
log = [] on the class bodyyesno
def __init__(self, log=[])yes, if callers skip logno
self.log = given_listyes, if two agents get the same listonly 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:

python
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.

python
def done(step, max_steps):
    return step >= max_steps

Do 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 havePick
One job, little saved stateA function
Several values plus actionsA class
Only a pile of fields, almost no behaviorA 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 run again in a test and not noticing step continued.
  • A method that only prints, so tests have nothing to assert.
Live PythonOpen full playgroundpython
Output
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

When is a function enough, with no class?