Classes And Objects
A class is a blueprint. An object is one thing built from it.
If you come from Java or C#, you already know this idea thoroughly — skip to the comparison near the bottom and note the differences. If you have never programmed before, the honest guidance is that you need less of this than you expect. Most agent code is functions and dictionaries. Classes turn up when you are holding state.
The shape
class Agent:
def __init__(self, name, model):
self.name = name
self.model = model
self.history = []
def ask(self, question):
self.history.append(question)
return f"{self.name} answering with {self.model}: {question}"
bot = Agent("triage", "deepseek-chat")
print(bot.ask("why is the server down?"))
print(bot.history)Reading it:
class Agent:starts the blueprint__init__is the constructor. It runs when you create an objectselfis the object itself, and it is written out explicitly every timeself.name = namestores a value on this particular objectAgent("triage", "deepseek-chat")builds one. There is nonew
self, which is the part that looks odd
Every method takes self as its first parameter, and you never pass it when calling.
bot.ask("hello")Python turns that into Agent.ask(bot, "hello") behind the scenes. The object goes in as self.
Two rules follow, and forgetting either is the classic beginner error:
- every method must declare
selfas its first parameter - every reference to the object's own data needs
self.— plainnameinside a method means a local variable, notself.name
class Agent:
def __init__(self, name):
self.name = name
def greet(self):
print(f"I am {self.name}")Java and C# let you write name and infer this. Python does not. It is more typing and less ambiguity.
Instance data versus class data
class Agent:
provider = "deepseek"
def __init__(self, name):
self.name = name
a = Agent("triage")
b = Agent("summary")
print(a.provider, b.provider)
print(a.name, b.name)provider is defined on the class, so every object shares it. name is set on self, so each object has its own.
The same trap as mutable default arguments applies here. A list defined at class level is shared by every instance:
class Agent:
history = []Every agent would append to the same list. Put mutable state in __init__ instead.
When a class earns its place
Use one when you have data and behaviour that belong together, and the data persists between calls.
Good reasons:
- an API client holding a key, a base URL and a session
- a conversation holding its message history
- anything you will create several of, each with its own state
Poor reasons:
- a place to put functions that do not share state — use a module
- one method and no data — use a function
- because you are used to everything living in a class
A useful test: if every method would work identically as a plain function taking the same arguments, you do not need a class.
A realistic example
class Conversation:
def __init__(self, system_prompt):
self.messages = [{"role": "system", "content": system_prompt}]
def add_user(self, text):
self.messages.append({"role": "user", "content": text})
def add_assistant(self, text):
self.messages.append({"role": "assistant", "content": text})
def last(self):
return self.messages[-1]["content"]
def __len__(self):
return len(self.messages)
chat = Conversation("You are a triage assistant.")
chat.add_user("The database is down.")
chat.add_assistant("Checking the logs now.")
print(len(chat))
print(chat.last())This is a class worth writing. The message list has to persist, several methods act on it, and you may want more than one conversation at a time.
Note __len__. Defining it makes len(chat) work. Python has a set of these dunder methods — __str__ for printing, __eq__ for comparison — that let your object behave like a built-in one.
Inheritance, lightly
class Tool:
def run(self, **kwargs):
raise NotImplementedError
class WeatherTool(Tool):
def run(self, city):
return f"28 degrees in {city}"class WeatherTool(Tool) means WeatherTool is a Tool and gets everything it has.
You will meet this when subclassing something a framework provides — a custom retriever, a custom callback handler. You will rarely need deep hierarchies of your own. Python favours composition, and for tools it favours plain decorated functions over classes entirely.
Privacy, or the lack of it
class Agent:
def __init__(self):
self.name = "triage"
self._retry_count = 0There is no private. A leading underscore means "internal, please leave alone". Nothing enforces it.
Coming from an enterprise codebase this feels reckless. In practice it causes far less trouble than you would expect, and it makes debugging and testing considerably easier.
Coming from Java or C#
| Concept | Java / C# | Python |
|---|---|---|
| Constructor | same name as the class | __init__ |
| This object | this, usually implicit | self, always explicit |
| Creating | new Agent() | Agent() |
| Inheritance | extends, : | class B(A): |
| Access control | private, public | convention only, _name |
| Interfaces | interface | Protocol, or duck typing |
| One class per file | usually enforced | not at all |
| Properties | getters and setters | @property |
| toString | toString, ToString | __str__ |
The habits to drop:
- You do not need a class to hold functions. A module of functions is normal.
- You do not need an interface to accept something. If it has the right method, it works.
- Getters and setters are not the default. Public attributes are fine; add
@propertylater if you need logic, without changing how callers use it.
Common mistakes
- Forgetting
selfin a method signature, givingtakes 0 positional arguments but 1 was given. - Forgetting
self.inside a method, so you silently create a local variable. - Mutable class attributes shared across every instance.
- Writing a class where a function would do.
- Expecting
_nameto be private. It is not.
Practise this
- Write a
Ticketclass with an id, a severity and a method that returns whether it is urgent. - Create two objects from it and confirm they have separate data.
- Add
__str__so thatprint(ticket)reads well. - Take a class you wrote and ask honestly whether a function would have been simpler.