Home / Articles

Gemini API in Python: The Same Stateless Loop With model Roles and Chat Objects

Call Gemini from Python with the google-genai SDK: contents and parts, system_instruction, token usage and a chat object that tracks history on the client.

Publisher: REACTAPP.TOP4 min read892 words

This article is published in English.

Sources: Part 4 — Call the Gemini API in Python (user / model, same loop)

After OpenAI and Claude, Google's Gemini is the third provider to show the same pattern: a stateless endpoint, a conversation tagged by role and the full history sent on every call. Gemini follows it with one pleasant quirk in its naming.

Renames, not a redesign

Gemini's conversation shape matches OpenAI's and Claude's apart from two renames. The assistant role is called model. That label is arguably more honest, since "assistant" hints at a persona while model just says the text came out of a model. It occupies the same slot in the list. The system prompt becomes system_instruction, passed in a config object and not in the message list. That's Claude's "system on the side" idea taken one level further.

The list of messages goes by contents. Every turn in it contains parts, since Gemini handles text, images and audio natively. With plain text chat none of that shows: strings go in, and resp.text comes out.

One warning about libraries before any code. Google has shipped several Python packages over the years, and older tutorials are full of deprecated imports such as google-generativeai and Bard-era wrappers. The current official SDK is google-genai, imported with from google import genai, and it's the one used here.

Setup and a single turn

Install it with pip install google-genai, create a key in Google AI Studio and export it as GEMINI_API_KEY. The client picks the key up from the environment, so no key ever appears in code.

# current official SDK: pip install google-genai
from google import genai
from google.genai import types

client = genai.Client()  # key from env

resp = client.models.generate_content(
    model="gemini-2.5-flash",
    contents="Why does Gemini name the reply "
             "role 'model', not 'assistant'?",
    config=types.GenerateContentConfig(
        system_instruction="Be concise.",
        temperature=0,
    ),
)
print(resp.text)

Set against the earlier two providers, messages is now contents, and a single turn can be a bare string. The system prompt goes into GenerateContentConfig. Temperature is a normal config field again, which the current Claude SDK took away. The answer is just resp.text, and token counts sit on resp.usage_metadata as prompt_token_count and candidates_token_count, the same measurements with Google's own names. Always log them.

A managed history loop

Porting your own ask() function directly is possible: store user and model turns in a list, append each exchange and pass contents= again each time. It works. Yet the SDK includes a convenience the OpenAI and Anthropic clients lack out of the box: a chat object that handles the appending.

chat = client.chats.create(
    model="gemini-2.5-flash",
    config=types.GenerateContentConfig(
        system_instruction="Be concise.",
    ),
)

r1 = chat.send_message(
    "Define a context window."
)
print(r1.text)

r2 = chat.send_message(
    "Now for a five-year-old."
)
print(r2.text)

for turn in chat.get_history():
    part = turn.parts[0].text[:40]
    print(turn.role, "->", part)

The follow-up gets answered in context because the chat object quietly replayed the earlier exchange, which is what a hand-rolled function would do. Loop over get_history() at the end and you see alternating user and model turns, the very list that was sent on the last request. Nothing server-side remembers anything. The chat object saves typing on the client, so input tokens still pile up each turn and long chats cost more per call.

One thing hasn't changed: the service behind the SDK remembers nothing between requests.

Convenience has a price, and it matters later with frameworks. A managed history means less code and fewer bugs, right up to the moment you must trim old turns, summarize them or slip retrieved documents into the middle of a conversation. Then you'll want the plain list back. LangChain sits at the same point on this scale, a level higher. Be comfortable with both the hand-kept list and the managed object, and neither can box you in.

Running the example

pip install -r requirements.txt
export GEMINI_API_KEY="AIza..."
python examples/part04_gemini.py

The script covers three things in order: a single turn, the manual-list loop to confirm it's equivalent, and the managed chat with its history printed. Token counts get logged after every call. With no key set it stops immediately with a clear message. Since the calls are live and billed, don't run it in CI.

Three providers, one shape

A quick comparison of all three. OpenAI treats system as a message and leaves history to you. Claude makes system a separate parameter, demands max_tokens and also leaves history to you. Gemini puts system_instruction into config, names the assistant model and will keep the history if you allow it. All three sit on the same stateless loop: send the full conversation, receive a reply, append it and go again.

That's why multi-provider frameworks can stay thin, and why switching providers needn't be scary. Migrating is a rename map, not a rewrite.