Talking to an AI Model from Python
Now for the part that makes this an AI app. In this lesson you make your first call to a language model from Python, read the response, and wrap it in a clean function. We use a plain API request. No agents, no orchestration framework, just a normal HTTP-style call, the same pattern you would use for any web API.
The code in this lesson talks to a live service over the internet, so it cannot run inside the in-page playground (the playground has no network access or API key). You will run these on your own machine. Read them carefully here first, then copy them into a local file when you set up a key.
What You'll Learn
- The mental model of a chat API: messages in, message out
- How to call a model with the official Python SDK
- Where the API key goes and how to keep it out of your code
- How to pull the text out of the response
- How to wrap the call in a reusable, provider-neutral function
The mental model: messages in, message out
Every modern chat model works the same way. You send a list of messages, each with a role (system, user, or assistant), and you get back one message of text. That is the entire interaction.
- A system message sets the behavior ("You are a precise data analyst").
- A user message is your actual request.
- The model replies as an assistant message.
Once you internalize "messages in, one message out," every provider looks familiar, because they all copy this shape.
Get a key and protect it
To call a real model you need an API key from a provider. Two common choices as of 2026 are Anthropic (Claude) and OpenAI (GPT). Both give you a key string that proves who is paying for the request. Sign up on the provider's site, create an API key, and add a small amount of credit (these calls cost a fraction of a cent each).
Never paste the key directly into your code. If you do, it can leak the moment you share the file or push it to GitHub. Instead, store it in an environment variable and read it at runtime. The simplest cross-platform way during development is a .env file plus the python-dotenv package.
Create a file named .env next to your script:
ANTHROPIC_API_KEY=your-key-goes-here
Then add .env to your .gitignore so it never gets committed. Now your code reads the key from the environment, and the secret lives outside the codebase.
Install what you need
In a terminal, inside your project folder:
pip install anthropic python-dotenv
If you prefer OpenAI, install openai instead. The shapes are nearly identical, and we show both below.
Your first call (Claude)
import os
from dotenv import load_dotenv
from anthropic import Anthropic
load_dotenv() # reads .env into the environment
client = Anthropic(api_key=os.environ["ANTHROPIC_API_KEY"])
response = client.messages.create(
model="claude-sonnet-4-6", # a current Claude model as of 2026
max_tokens=300,
system="You are a precise data analyst. Be concise.",
messages=[
{"role": "user", "content": "In one sentence, what does a median tell you that a mean does not?"}
],
)
# The reply text lives inside the response object:
print(response.content[0].text)
Three things to notice:
load_dotenv()plusos.environ[...]keeps the key out of the code.modelis a specific, pinned model name. Always name a specific model rather than relying on a moving alias.max_tokenscaps how long the reply can be, which protects you from runaway costs.
The same call (OpenAI)
If you went with OpenAI, the structure barely changes. The system instruction simply becomes another message in the list.
import os
from dotenv import load_dotenv
from openai import OpenAI
load_dotenv()
client = OpenAI(api_key=os.environ["OPENAI_API_KEY"])
response = client.chat.completions.create(
model="gpt-4o-mini", # a current, low-cost OpenAI model as of 2026
max_tokens=300,
messages=[
{"role": "system", "content": "You are a precise data analyst. Be concise."},
{"role": "user", "content": "In one sentence, what does a median tell you that a mean does not?"},
],
)
print(response.choices[0].message.content)
The only real differences are the import, the method name, where the system prompt lives, and how you reach into the response (response.choices[0].message.content). Same idea, different wrapper.
Wrap it in one function
Your app should not care which provider you chose. Hide the details behind a single function that takes a prompt and returns text. This is the function the rest of the app will call.
import os
from dotenv import load_dotenv
from anthropic import Anthropic
load_dotenv()
_client = Anthropic(api_key=os.environ["ANTHROPIC_API_KEY"])
def ask_ai(prompt, system="You are a precise data analyst. Be concise.", max_tokens=600):
"""Send a prompt to the model and return the reply as plain text."""
response = _client.messages.create(
model="claude-sonnet-4-6",
max_tokens=max_tokens,
system=system,
messages=[{"role": "user", "content": prompt}],
)
return response.content[0].text
# Quick test
if __name__ == "__main__":
print(ask_ai("Give me two bullet points on why outliers matter in averages."))
Now the rest of your code just calls ask_ai("...") and gets a string back. If you ever switch providers, you change this one function and nothing else breaks. That is the payoff of a thin wrapper.
What about cost and limits
A few habits keep you safe and cheap:
- Set
max_tokensto something reasonable. You almost never need thousands of output tokens for a summary. - Send small inputs. This is exactly why the next lesson focuses on summarizing your data before sending it. Sending a whole 50,000-row CSV is slow, expensive, and usually worse than sending a tight summary.
- Add a spending limit in your provider dashboard while you are learning, so a bug cannot run up a bill.
Key Takeaways
- A chat API is "messages in, one message out," with
system,user, andassistantroles. - Keep your API key in an environment variable (a
.envfile pluspython-dotenv), never in code, and gitignore it. - Name a specific, pinned model and cap output with
max_tokens. - Anthropic and OpenAI use nearly identical patterns; the response field names differ.
- Hide the provider behind one
ask_ai(prompt)function so the rest of the app stays simple and swappable.

