API Beginner easy · 5 min

None content: tool call messages

What you will learn
When an LLM calls a tool but you haven't executed it yet, the message content is <code>None</code>: you must handle this state before sending the result back.

Why this matters

Tool use workflows require you to detect when the model wants to call a function, execute that function externally, and return the result. If you don't handle <code>None</code> content correctly, your tool loop breaks or sends invalid messages back to the API.

Skip if: If you're using an agent framework that manages the tool loop automatically (like LangChain's AgentExecutor or Anthropic's tool_use_block_handler), you don't directly handle None content: the framework does. Only use this pattern when building custom tool orchestration.

Explanation

When you call client.chat.completions.create() with tools defined, the model can respond with role='assistant' and content=None. This doesn't mean the response failed: it means the model chose to call a tool instead of generating text.

The actual tool call lives in message.tool_calls, which is a list of ToolCall objects. Each contains id, function.name, and function.arguments (a JSON string). The None content indicates 'I'm not talking to you directly; I'm invoking a function.' You must parse the tool calls, execute them in your own code, and send the results back as role='tool' messages with the matching tool_call_id.

This is the core of agentic patterns: the model is now a planner that delegates work, not a content generator. You are the executor. If you ignore the None and try to display it or continue without tool results, the conversation state becomes inconsistent.

Request code

python
import json
from openai import OpenAI

client = OpenAI()

tools = [
    {
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "Get the current weather for a city",
            "parameters": {
                "type": "object",
                "properties": {
                    "city": {"type": "string", "description": "City name"}
                },
                "required": ["city"]
            }
        }
    }
]

messages = [{"role": "user", "content": "What's the weather in Boston?"}]

response = client.chat.completions.create(
    model="gpt-4o",
    messages=messages,
    tools=tools
)

print(f"Response choice 0:")
print(f"  role: {response.choices[0].message.role}")
print(f"  content: {response.choices[0].message.content}")
print(f"  tool_calls: {response.choices[0].message.tool_calls}")

if response.choices[0].message.tool_calls:
    tool_call = response.choices[0].message.tool_calls[0]
    print(f"\nTool call detected:")
    print(f"  id: {tool_call.id}")
    print(f"  function: {tool_call.function.name}")
    print(f"  arguments: {tool_call.function.arguments}")

Authentication

Set your OpenAI API key before instantiation: either via environment variable OPENAI_API_KEY or pass api_key directly to OpenAI(api_key='sk-...').

Response shape

FieldDescription
choices[0].message.role "assistant"
choices[0].message.content None when tool_calls are present
choices[0].message.tool_calls [ToolCall(...), ...]
choices[0].message.tool_calls[0].id "call_abc123..."
choices[0].message.tool_calls[0].function.name "get_weather"
choices[0].message.tool_calls[0].function.arguments "{\"city\": \"Boston\"}"

Field guide

content

Will be None when tool_calls are populated. Do not assume this field is always a string.

tool_calls

List of ToolCall objects. This is where the actual function invocation data lives. Each has a unique id you must echo back in your tool result message.

tool_call_id

Critical: you must save this id and include it in the role='tool' message when you send back the function result. Mismatching ids breaks the conversation context.

Setup trap

After you execute a tool and generate a result, you must append BOTH the original assistant message (with tool_calls) AND a new tool message to your messages list before the next API call. Forgetting to include the assistant message breaks the conversation chain and the model won't remember why it called the tool.

Cost

Tool calls themselves are free: they count as normal completion tokens. However, each back-and-forth in a tool loop uses tokens (input on the retry, new output generated). A 5-step tool loop costs roughly 5x a single completion.

Rate limits

Tool loops can trigger rate limits faster because each tool result + follow-up is a separate request. If you're making 10 tool calls in one workflow, that's 20 API calls. Implement backoff or batch tool execution if doing this at scale.

Common gotcha

Developers often check if response.choices[0].message.content: and assume there's always text to display. With tool calls, content is None, and this check fails silently. You must check response.choices[0].message.tool_calls separately and branch accordingly.

Error recovery

AttributeError: 'NoneType' object has no attribute ...
You're trying to access message.content as if it's always a string. Check if tool_calls exist first: if message.tool_calls: handle_tools() else: print(message.content)
tool_call_id not found in response
The model decided not to call the tool you defined. Check your tool definitions are valid JSON schema and the function names match your handler logic.
messages with tool results rejected
You sent a role='tool' message without the corresponding role='assistant' message that contains the tool_call_id. Always append the assistant message first, then the tool result message.

Experienced dev note

None content is actually a signal: it's the model saying 'I've delegated this to you.' Treat it as a state machine: model generates None → you execute → you return result → model continues. Many developers build tool loops that leak tool results without the assistant message, causing the model to lose context. The cost multiplies fast. Always preserve the full message chain.

Check your understanding

You call the API with tools defined. The response has content=None and tool_calls=[...]. You execute the tool and get a result string. Write the exact structure of the two messages you append to the messages list before the next API call.

Show answer hint

The first message appended should echo the assistant's response (including the tool_calls), and the second should be a tool message with the result and matching tool_call_id.

VERSION openai>=1.0.0 uses the ToolCall object model. Earlier SDK versions used a different structure. Ensure you're on 1.x by running pip install --upgrade openai.

Community Notes

No notes yetBe the first to share a version-specific fix or tip.