By the end of this guide you will have the same question answered twice — once by OpenAI's gpt-4o-mini and once by Google's gemini-2.5-flash — plus one ask() function that talks to either provider through a single argument. Budget about thirty minutes, most of it spent collecting two API keys. You need no experience beyond having made one successful API call before.
An API key is a long secret string that identifies your account to a provider's servers, and an SDK (software development kit) is the Python package that wraps their web endpoints so you can call them like ordinary functions. OpenAI and Google each publish their own SDK, and the two look different enough on the page to make beginners think they are learning two unrelated skills. They are not. The differences fit in a short table, and this guide walks through every one of them with code you can run.
This is one guide in Understanding LLM APIs, written for people who can run a Python file but have never shipped production code. If you want the same treatment for a different pair, OpenAI vs Anthropic API for Beginners covers Claude.
Prerequisites
You need Python 3.10 or newer and a working virtual environment — an isolated folder of packages that keeps this project's libraries separate from the rest of your machine. If that phrase is new, work through Create a Python Virtual Environment for AI first, then come back.
Install both SDKs at once. The Gemini package is called google-genai; an older package named google-generativeai exists and is a different library with different function names, so be precise here.
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install "openai>=1.40" "google-genai>=1.0" "python-dotenv>=1.0"
pip freeze > requirements.txt
Collect one key from each provider's console and put them side by side in a file named .env in your project folder:
OPENAI_API_KEY=sk-your-openai-key-here
GEMINI_API_KEY=your-google-ai-studio-key-here
Now add that file to .gitignore before you write another line:
echo ".env" >> .gitignore
Skipping this step is how people wake up to a four-figure bill. Automated scanners crawl public repositories for key patterns within minutes of a push, so treat the .gitignore line as part of the install, not as tidying up afterwards.
Step 1 — Make the call with the openai package
Start with the SDK you are most likely to have seen already. The openai package builds a client object from your key, and every text request goes through client.chat.completions.create.
import os
from dotenv import load_dotenv
from openai import OpenAI
load_dotenv() # reads .env, which is listed in .gitignore
client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": "You explain things in plain English."},
{"role": "user", "content": "What is an API key? Answer in two sentences."},
],
max_tokens=200,
)
print(response.choices[0].message.content)
print("Tokens used:", response.usage.total_tokens)
Three things are worth naming. The instruction that sets the model's behaviour travels inside the messages list as a dictionary tagged "role": "system" — it is just another message, sitting before the user's. The reply arrives at response.choices[0].message.content, and that [0] exists because the endpoint can return several alternative answers in one call. Finally, max_tokens caps the length of the reply; a token is roughly three-quarters of a word, and both input and output tokens are billed.
Save the file as ask_openai.py and run python ask_openai.py. If you get a 401, your key never reached the client — Fix the 401 Unauthorized Error in OpenAI Python walks through the causes.
Step 2 — Make the identical call with google-genai
Now the same question, the same system instruction, the same length cap — written for Gemini. The import line is unusual: you import genai from the google namespace, and the helper classes come from google.genai.types.
import os
from dotenv import load_dotenv
from google import genai
from google.genai import types
load_dotenv() # reads .env, which is listed in .gitignore
client = genai.Client(api_key=os.getenv("GEMINI_API_KEY"))
response = client.models.generate_content(
model="gemini-2.5-flash",
contents="What is an API key? Answer in two sentences.",
config=types.GenerateContentConfig(
system_instruction="You explain things in plain English.",
max_output_tokens=200,
),
)
print(response.text)
print("Tokens used:", response.usage_metadata.total_token_count)
Compare the two files line by line and four differences stand out. The prompt goes into contents as a plain string rather than a list of role-tagged dictionaries — you may pass a list when you want a conversation, but a single turn needs no ceremony. The system instruction is not a message at all; it lives in a config object built from types.GenerateContentConfig. The length cap is spelled max_output_tokens inside that same config. And the reply is a plain attribute, response.text, with no index to remember.
Neither shape is better. OpenAI's list-of-messages is honest about the fact that a system instruction really is part of the conversation; Gemini's config object keeps per-call settings in one visible place, which is pleasant once you start tuning several of them at once. The diagram below traces the same prompt down both paths.
The two designs diverge again when you want a back-and-forth conversation. With OpenAI you keep appending to the messages list yourself: user turn, assistant turn, user turn, and you resend the whole list every time. Gemini supports the same manual approach — pass a list to contents instead of a string — but also offers client.chats.create(...), which returns a chat object that remembers the history for you and exposes a send_message method. Beginners often prefer the chat object; anyone building a real app usually goes back to managing the list by hand, because that is the only way to trim old turns when a conversation grows too long to send.
Step 3 — Send an image to each provider
Both providers accept pictures alongside text, which is the point at which the two SDKs feel most different. OpenAI expects the image encoded as base64 — a way of writing binary data using ordinary letters and digits — and pasted into a data URL inside the message's content list.
import base64
import os
from dotenv import load_dotenv
from openai import OpenAI
load_dotenv()
client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
with open("chart.png", "rb") as f:
encoded = base64.b64encode(f.read()).decode("utf-8")
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{
"role": "user",
"content": [
{"type": "text", "text": "Describe this chart in one sentence."},
{"type": "image_url",
"image_url": {"url": f"data:image/png;base64,{encoded}"}},
],
}],
max_tokens=200,
)
print(response.choices[0].message.content)
Gemini hands you a helper instead. types.Part.from_bytes takes the raw bytes and a MIME type — the short label such as image/png that tells a server what kind of file it is receiving — and you drop the result straight into the contents list next to your question.
import os
from dotenv import load_dotenv
from google import genai
from google.genai import types
load_dotenv()
client = genai.Client(api_key=os.getenv("GEMINI_API_KEY"))
with open("chart.png", "rb") as f:
image_bytes = f.read()
response = client.models.generate_content(
model="gemini-2.5-flash",
contents=[
types.Part.from_bytes(data=image_bytes, mime_type="image/png"),
"Describe this chart in one sentence.",
],
)
print(response.text)
The gap widens past images. Gemini models take audio and video as ordinary parts of the same generate_content call, so a clip and a question travel together. OpenAI splits that work across endpoints: speech goes to the transcription API first, and you feed the resulting text into a chat call. Neither approach is wrong, but the shape of your script changes depending on which you pick.
Step 4 — Hide both behind one ask() function
Once you can write either call, stop choosing. Put both behind a function with a provider argument and the rest of your program stays provider-agnostic — you can swap models with one keyword and compare answers without touching any other code. The temperature argument is a dial from roughly 0 to 1 controlling how varied the wording is; What Is Temperature in an LLM API? explains what to set it to. Both providers accept it under the same name, which makes it easy to forward.
import os
from dotenv import load_dotenv
from openai import OpenAI
from google import genai
from google.genai import types
load_dotenv() # both keys come from .env, which is in .gitignore
openai_client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
gemini_client = genai.Client(api_key=os.getenv("GEMINI_API_KEY"))
SYSTEM = "You are a concise assistant. Answer in plain English."
def ask(prompt: str, provider: str = "openai", temperature: float = 0.2) -> str:
"""Send one prompt to either provider and return the reply as a string."""
if provider == "openai":
response = openai_client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": SYSTEM},
{"role": "user", "content": prompt},
],
temperature=temperature,
max_tokens=400,
)
print(f"[openai tokens: {response.usage.total_tokens}]")
return response.choices[0].message.content
if provider == "gemini":
response = gemini_client.models.generate_content(
model="gemini-2.5-flash",
contents=prompt,
config=types.GenerateContentConfig(
system_instruction=SYSTEM,
temperature=temperature,
max_output_tokens=400,
),
)
print(f"[gemini tokens: {response.usage_metadata.total_token_count}]")
return response.text
raise ValueError(f"Unknown provider: {provider!r}")
if __name__ == "__main__":
question = "Name two things worth logging on every AI API call."
print("OpenAI:", ask(question, provider="openai"))
print("Gemini:", ask(question, provider="gemini"))
Run it with python ask_both.py. You get two answers to one question and a token count for each, printed in the same format, which is the fastest honest way to judge quality and cost on your own task rather than someone else's benchmark. Keep that print line: usage reporting is the habit that stops surprise bills, and Count Tokens in Python Before You Send shows how to estimate the cost before the call goes out.
Quick reference
Port a script from one SDK to the other and these eight rows are almost always the entire diff.
| Concern | OpenAI | Gemini |
|---|---|---|
| Install | pip install openai | pip install google-genai |
| Import | from openai import OpenAI | from google import genai |
| Client | OpenAI(api_key=...) | genai.Client(api_key=...) |
| Call method | client.chat.completions.create | client.models.generate_content |
| Prompt argument | messages=[{...}] | contents="..." |
| System instruction | message with role: "system" | system_instruction in config |
| Reply text | response.choices[0].message.content | response.text |
| Token counts | response.usage.total_tokens | response.usage_metadata |
The output cap is the one extra trap: OpenAI calls it max_tokens at the top level, Gemini calls it max_output_tokens inside the config object.
Free tiers, honestly
This is the difference beginners feel first. Google's AI Studio hands you a key that works on a free tier for several Gemini models, limited by requests per minute and per day. It is enough to learn on, enough for a personal script, and not enough for anything with users. OpenAI's platform expects billing details before your first call; new accounts sometimes receive trial credit, but you should plan on paying from day one.
Do not treat any specific quota number you read online as current — including in tutorials that look freshly written. Both providers change limits and prices regularly, so open the provider's own rate-limit and pricing pages when you need a figure. If a genuinely free key is what you need right now, Best Free AI APIs for Beginners lists the options, and Groq vs OpenRouter Free Tier compares two more.
One practical consequence: free tiers rate-limit aggressively, so a loop over a spreadsheet will hit a wall long before your logic is wrong. Add a short pause between calls and retry on failure rather than assuming your code broke.
There is a second, less obvious consequence. Free tiers on any provider usually come with looser data-handling terms than paid usage — your prompts may be eligible for review or model training. That is fine for practice prompts and terrible for a client's contract or a customer's support ticket. Read the terms attached to the tier you are on before you paste anything you would not email to a stranger, and move to a paid key the moment real data enters your script.
Troubleshooting
ModuleNotFoundError: No module named 'google.genai'— you installed the oldergoogle-generativeaipackage, or you installed into a different environment from the one running your script. Activate the virtual environment, then runpip install google-genai. The general fix is in Fix ModuleNotFoundError: No Module Named openai.TypeError: generate_content() got an unexpected keyword argument 'system_instruction'— you passed the system instruction directly to the call. It belongs insideconfig=types.GenerateContentConfig(system_instruction=...), andtypesmust be imported fromgoogle.genai.AttributeError: 'GenerateContentResponse' object has no attribute 'choices'— you copied OpenAI's reply-reading line into the Gemini script. Change it toresponse.text.429 RESOURCE_EXHAUSTED(Gemini) or a429rate-limit error (OpenAI) — you sent requests faster than the tier allows. Slow the loop down and retry with an increasing wait; Fix the 429 Rate-Limit Error in Python has the retry pattern.response.textprintsNone— the model returned no usable candidate, usually because a safety filter stopped it or the output cap cut it off before any text was produced. Printresponse.candidatesto see the finish reason before you change the prompt.
When to use this vs. alternatives
Neither provider wins outright, and anyone telling you otherwise is selling something. Match the choice to the job in front of you.
- Reach for Gemini when you want to write code today without entering payment details, or when your input is a long document, an audio file or a video and you would rather send it in one call than build a pipeline around it.
- Reach for OpenAI when you want the largest pool of tutorials and Stack Overflow answers behind you, or when your project also needs image generation, embeddings or speech synthesis under a single key and library.
- Reach for a third option when neither fits: Claude is the strongest alternative for long careful writing and tool use, and free aggregator endpoints are better for pure experimentation.
If you keep the ask() function from Step 4, none of this is permanent. Swap the provider argument, rerun your script, and compare the two answers on your own task — that beats every comparison article, including this one. Start with whichever key you can get in the next five minutes, write something that works end to end, and add the second provider the day a task actually needs it.
Back to Understanding LLM APIs.
Related guides
- What Is Temperature in an LLM API? — the one setting both SDKs share, and what to set it to.
- OpenAI vs Anthropic API for Beginners — the same side-by-side treatment for Claude.
- Write System Prompts that Control Output Format — make either provider return the shape you asked for.
- Fix the 429 Rate-Limit Error in Python — the error you will meet first on a free tier.
- Summarize Long PDFs with Python and Chunking — what to do when a document is too big even for a large input window.