Model a chat conversation with a Message dataclass, then serialize a list of messages into the exact format an LLM API expects: a list of dictionaries, each with a role and a content key.
Every chat request you send is a list of these message dictionaries. Using a dataclass to represent a message gives you type checking, sensible defaults, and a single place to define the structure, instead of passing around loose dictionaries that are easy to typo.
Message dataclass with role and content fields.Message objects into a list of {"role": ..., "content": ...} dictionaries.The serialized output is the message list you would pass straight to client.chat.completions.create(messages=...).
asdict from the dataclasses module turns a dataclass instance into a dictionary whose keys are the field names, in the order they were declared. That gives you the {"role": ..., "content": ...} shape for free, so the serialization function is a single comprehension. Because the structure lives in the Message definition, changing the message shape later means editing one class rather than every place that builds a dictionary by hand.
Wrap the message list in a Conversation class that keeps itself within a maximum size. Each time you add a message, the class drops the oldest messages so the conversation never grows past max_messages.
This bundles state and behavior the way real chat clients do: the conversation owns its messages and its trimming rule, so callers just add and never worry about the window. It is the object-oriented version of the memory management you will see again with agents.
Conversation a max_messages limit and a list of messages.add(role, content) to append a Message and then drop the oldest until the list fits max_messages.max_messages=3 and print what remains.Only the three most recent messages survive.
Putting the trimming logic inside add means the invariant (never more than max_messages) holds automatically after every call, so no caller can forget to enforce it. The dataclass gives you the message structure and a clean constructor for free. This is the same pin-and-window idea used for agent memory, expressed as a small object that manages its own state.