How to Build a System-User-Assistant Message List in Python
Use dataclasses to model a chat conversation and build the system/user/assistant message list expected by LLM APIs.
Python code
33 linesfrom dataclasses import dataclass, field
from typing import List
@dataclass
class Message:
role: str
content: str
@dataclass
class Conversation:
messages: List[Message] = field(default_factory=list)
def add_system(self, content: str) -> None:
self.messages.append(Message(role="system", content=content))
def add_user(self, content: str) -> None:
self.messages.append(Message(role="user", content=content))
def add_assistant(self, content: str) -> None:
self.messages.append(Message(role="assistant", content=content))
def to_dict_list(self) -> List[dict]:
return [{"role": m.role, "content": m.content} for m in self.messages]
if __name__ == "__main__":
convo = Conversation()
convo.add_system("You are a helpful coding assistant.")
convo.add_user("How do I sort a list in Python?")
convo.add_assistant("Use the sorted() function or the list.sort() method.")
print(convo.to_dict_list())
Output
[{'role': 'system', 'content': 'You are a helpful coding assistant.'}, {'role': 'user', 'content': 'How do I sort a list in Python?'}, {'role': 'assistant', 'content': 'Use the sorted() function or the list.sort() method.'}]
How it works
The Conversation dataclass wraps a list of Message objects, each storing a role and content string. The add_system, add_user, and add_assistant methods append correctly typed messages to the list, keeping the call site simple and readable. The to_dict_list method converts each Message into a plain dict with role and content keys, which is the exact structure most LLM APIs (OpenAI, Anthropic, etc.) expect. Using a field(default_factory=list) avoids the classic mutable-default-argument bug that would share one list across all Conversation instances. This pattern keeps your chat history explicit and easy to extend with extra fields like name or tool_calls later.
Common mistakes
- Using `field(default=[])` instead of `field(default_factory=list)` — this shares one mutable list across all instances.
- Forgetting to include the system message, which many LLM APIs require for behavior instructions.
- Hard-coding role strings as raw literals in every call instead of using dedicated add_ methods, making typos like 'Asssistant' likely.
Variations
- Use a list of dicts directly and append with `{'role': 'system', 'content': ...}` for quick scripts.
- Add a `from_dict_list` classmethod to load existing chat logs back into a `Conversation`.
Real-world use cases
- Assembling the multi-turn message payload for an OpenAI or Anthropic chat completion call.
- Logging and replaying conversation history in a support chatbot for debugging or fine-tuning datasets.
- Building prompt templates that interleave system instructions, user questions, and prior assistant answers for few-shot prompting.
Sponsored
More from AI & LLM integration patterns
- Cache LLM Completions by Hashing the Prompt in Python easy
- Chain of Thought Prompting in Python: Step-by-Step Reasoning Demo easy
- Circuit Breaker Pattern in Python for LLM API Calls medium
- Cosine Similarity to Retrieve Top K Chunks in Python easy
- Demonstrate Prompt Injection Bypass in Python easy
- How to Accumulate Streamed Tokens into a Final String in Python easy
Keep learning
Related tutorials and quizzes for this topic.