Instance vs. class variables
Reading vs. reassigning — the shadowing trap
Reading a class variable through self works fine, since Python looks it
up on the instance first, then falls back to the class if it’s not found
there. But assigning to self.x doesn’t modify the class variable — it
creates a brand-new instance attribute that shadows it, for that instance
only:
agent_a.default_model = "claude-haiku" doesn’t touch the shared class
variable at all — it creates a new instance attribute on agent_a that
happens to have the same name, which now shadows the class variable
whenever you look it up through agent_a specifically. agent_b and
Agent itself are completely unaffected. This is the same shadowing logic
as reassigning a variable inside a function creating a new local instead of touching the outer one
— just one level up, instance vs. class instead of local vs. global.
The mutable class variable trap
This shadowing behavior is mostly harmless for immutable values like
strings or numbers — you get a separate copy, no real damage done. It gets
genuinely dangerous when the class variable is a mutable type, like a list,
and you mutate it in place instead of reassigning it. Mutating in place
(.append(), not =) never triggers the shadowing above — every instance
is still pointing at the exact same shared list:
agent_b never called add_tool, but it sees "search" anyway — both
instances share the exact same list object, so mutating it through one
instance is visible through every other one too. This is the same failure
shape as the mutable default argument trap:
a single mutable object created once and silently shared everywhere that
doesn’t explicitly get its own copy.
The fix is the same instinct as that trap’s fix — give each instance its
own copy, created fresh in __init__, not shared at the class level:
When a class variable is actually the right call
None of this means class variables are a mistake to avoid — they’re the right tool in two specific cases:
A genuine constant, the same for every instance and never meant to change per-object:
class Agent:
DEFAULT_TEMPERATURE = 0.7 # ALL_CAPS is the usual convention for a constant
def __init__(self, name: str, temperature: float = None):
self.name = name
self.temperature = temperature if temperature is not None else Agent.DEFAULT_TEMPERATURE(not run live — illustrating the pattern, not a new output)
A value genuinely meant to be shared and tracked across every instance
— like a running count of how many agents have been created. The key
detail: modify it through the class name, not self, or you’ll hit the
exact shadowing trap from above:
Agent.total_created += 1 explicitly targets the class variable — writing
self.total_created += 1 instead would silently create a separate,
shadowed instance attribute on each agent, starting over at a wrong value
every time, instead of actually incrementing a shared count.
What's the key difference between a class variable and an instance attribute?
Add a class variable total_calls starting at 0. In __init__, store tool_name, increment ToolCall.total_calls, and save the resulting count as an instance attribute so call_number() can return it later. Each new ToolCall should see the running total from every ToolCall created before it, including ones from previous test cases.