Fundamentals

Understanding LLM APIs: A Step-by-Step Python Guide for Beginners

Learn how LLM APIs work and call one from Python. Covers setup, secure keys, request crafting, parameters, and fixing the most common errors from scratch.

You have a task an AI could clearly help with: drafting replies, summarising a report, sorting messy notes. You have heard that large language models can do this. But every tutorial seems to assume you already know what an "endpoint" is, what a "token" costs, and why your first attempt returns a wall of red error text instead of an answer. That gap is what this guide closes.

A large language model API (application programming interface — a web address your program talks to) lets you borrow a powerful AI model over the internet. You never train it, host it, or even download it. Your Python script sends some text; a model running on the provider's servers writes a reply; the reply comes back to your script as data you can read and reuse. By the end of this guide you will install the right tools, store your key without leaking it, send a real request, understand every setting you can tune, and fix the four or five errors that trip up almost everyone on their first day.

This is one section of Python AI Fundamentals for Non-Developers, written for creators, marketers, founders, and students who are comfortable copying a command but have never shipped production code.

Who this is for and what you will build

You need this guide if you can run a Python file but have never made one talk to an AI service. The task is simple to state and surprisingly easy to get slightly wrong: take a string of text, send it to a model, and get a useful reply back, reliably, without exposing your billing key or blowing your budget.

We will build that piece by piece. First the environment, so nothing conflicts. Then secure key handling, so your credentials stay yours. Then a real request and a careful read of the response. Then the knobs you can turn to change how the model behaves. Each step is a runnable Python file, not a fragment, so you can paste it and watch it work.

The flow below is the whole mental model. Everything in this guide is a piece of this picture.

How a Python request travels through an LLM API Your Python script sends a prompt over HTTPS; the service tokenises the text, the model predicts tokens, and a JSON response with text and usage returns to your script. Your Python script prompt + settings Tokenizer text to tokens Model predicts tokens JSON response text + usage you send a prompt and parameters the reply returns on the same path
Every call is a round trip: your script sends text and settings, the service turns text into tokens, the model predicts a reply, and a JSON response carries the text and token usage back to you.

Prerequisites: setting up a clean environment

A clean, isolated workspace stops one project's libraries from breaking another's. Confirm you are on Python 3.10 or newer, since older versions reached end-of-life and miss features the modern SDK relies on:

python --version

If that prints anything below 3.10, install a current version first — the Setting Up Python for AI section walks through it for each operating system.

Now create a virtual environment (a private folder that holds this project's libraries) and install what you need:

python -m venv .venv
source .venv/bin/activate        # Windows: .venv\Scripts\activate
pip install "openai>=1.40" "httpx>=0.27" "python-dotenv>=1.0"
pip freeze > requirements.txt

We install three things. The openai SDK is the friendly, official wrapper that turns a network call into one Python function. httpx is a modern HTTP library; the SDK uses it under the hood, and we will use it directly once to show what is really happening on the wire. python-dotenv loads secrets from a file so they never live in your code. Pinning versions with pip freeze means the same code runs the same way next month and on a teammate's machine.

Next, store your key. Generate one in your provider's dashboard, then create a file named .env in your project folder:

OPENAI_API_KEY=sk-your-real-key-goes-here

Treat that key like the password to your bank. Immediately add .env to your .gitignore file so it is never committed or shared:

echo ".env" >> .gitignore

That one line is the difference between a private credential and a public one. A key pushed to a repository can be found and used by strangers within minutes, and the charges land on you.

Step 1: Send your first request with the openai SDK

With the environment ready, a working call is only a few lines. The pattern is always the same: load the key, create a client, then send a list of messages.

import os
from dotenv import load_dotenv
from openai import OpenAI

load_dotenv()  # reads .env and puts the key into the environment

client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))

response = client.chat.completions.create(
    model="gpt-4o-mini",
    messages=[
        {"role": "system", "content": "You are a concise assistant."},
        {"role": "user", "content": "Explain what a token is in one sentence."},
    ],
)

print(response.choices[0].message.content)

The messages list is a short conversation. A system message sets the model's role and rules; a user message is your actual request. The model reads both and writes an assistant message in reply. You get that reply at response.choices[0].message.content. The gpt-4o-mini model is small, fast, and cheap — perfect for learning. Run this file and you should see a single tidy sentence print to your terminal.

Step 2: Read the response and track your usage

The reply text is the headline, but the response carries more. The most important extra is usage — the count of tokens consumed, which is exactly what you are billed on. Logging it from day one keeps costs from surprising you.

print("Reply:", response.choices[0].message.content)
print("Why it stopped:", response.choices[0].finish_reason)

usage = response.usage
print(f"Prompt tokens:     {usage.prompt_tokens}")
print(f"Completion tokens: {usage.completion_tokens}")
print(f"Total tokens:      {usage.total_tokens}")

finish_reason tells you why the model stopped. "stop" means it finished naturally; "length" means it hit your max_tokens cap and was cut off mid-thought — a sign to raise the limit. The token counts let you estimate cost: multiply by the model's per-token price from the dashboard. A reply that cost a fraction of a cent today can cost real money at scale, so make this visible early.

There is one gap in this approach: usage only arrives after you have already paid for the call. When you are about to send a very long document, or loop the same request over a thousand rows of a spreadsheet, you want the number in advance. Measuring the prompt before it leaves your machine is the subject of Count Tokens in Python Before You Send, and the wider habits — budgets, caching, spend alerts — are collected in Managing AI API Costs and Tokens. For a first script, printing total_tokens on every call is enough discipline.

Step 3: See the raw HTTP call with httpx

The SDK hides the network so you can focus on your task, but it helps to see what it sends just once. Underneath, the SDK makes an ordinary HTTPS request — a POST with a header carrying your key and a JSON body carrying your prompt. Here is that same call written by hand with httpx:

import os
import httpx
from dotenv import load_dotenv

load_dotenv()

response = httpx.post(
    "https://api.openai.com/v1/chat/completions",
    headers={"Authorization": f"Bearer {os.getenv('OPENAI_API_KEY')}"},
    json={
        "model": "gpt-4o-mini",
        "messages": [{"role": "user", "content": "Say hello in five words."}],
    },
    timeout=30.0,
)

response.raise_for_status()          # turns a 4xx/5xx status into an exception
data = response.json()               # parse the JSON body into a dict
print(data["choices"][0]["message"]["content"])

Three things are worth noticing. The Authorization header is how the server knows the request is yours — a wrong or missing key here is exactly what causes a 401 error. The json= body is the payload the SDK builds for you automatically. And raise_for_status() plus response.json() are the manual steps the SDK normally does on your behalf. You will almost always prefer the SDK, but now the magic is no longer a mystery.

Every request you will ever send has the same three parts, whichever provider or library you use. Learning to name them makes error messages far easier to read, because each part fails in its own recognisable way.

The three parts of an LLM API request An annotated breakdown of an HTTPS request: the method and URL choose the endpoint, the Authorization header proves who you are, and the JSON body carries the model name, messages and parameters. What you send Why it matters POST + the URL /v1/chat/completions Picks the feature chat, images, audio Authorization Bearer sk-your-key Proves it is you wrong key gives 401 The JSON body model, messages and any parameters The actual work too big gives a 400 and burns tokens
Read any API error by asking which of the three parts it names: the URL, the key in the header, or the JSON body you built.

Step 4: Tune the model's behaviour with parameters

The same prompt can produce a wide range of replies depending on a handful of settings you pass alongside it. These control length, randomness, and format. Understanding them is the difference between fighting the model and directing it.

response = client.chat.completions.create(
    model="gpt-4o-mini",
    messages=[
        {"role": "system", "content": "You write short marketing taglines."},
        {"role": "user", "content": "A tagline for a calm productivity app."},
    ],
    temperature=0.9,     # higher = more varied, creative wording
    max_tokens=30,       # hard cap on the reply length
    top_p=1.0,           # alternative way to limit randomness
    n=3,                 # ask for three separate options at once
)

for i, choice in enumerate(response.choices, start=1):
    print(f"Option {i}: {choice.message.content}")

For a creative task, a higher temperature gives you variety; for a factual one, drop it near zero so answers stay consistent. Asking for n=3 returns three candidates in a single call, which is handy for brainstorming. The next section explains each setting in full. To go deeper on writing the messages themselves, the Prompt Engineering Basics section covers system prompts and output control.

Temperature is the setting beginners misuse most, so it is worth a concrete picture. It does not make the model smarter or dumber — it changes how boldly the model picks its next word when several are plausible. Low values make it choose the safest candidate almost every time, which is what you want for data extraction, classification, or anything you will parse. High values let it reach for the less obvious word, which is what you want for names, taglines, and first drafts. What Is Temperature in an LLM API? takes the setting apart in detail; the summary below is enough to choose a starting value.

The same prompt at a low and a high temperature One prompt branches into two settings: a low temperature produces near-identical answers on every run, while a high temperature produces fresh wording each time. One prompt run it five times temperature=0.2 picks the safe word temperature=0.9 explores rarer words Nearly the same answer every run Fresh wording on every run
Pick a low temperature whenever your code has to parse the answer, and a high one whenever a human is choosing between options.

Step 5: Stream the reply so it appears as it is written

By default your script waits in silence until the model has finished the entire reply, then receives it in one lump. For a long answer that can be ten or fifteen seconds of nothing. Streaming changes the shape of the response: instead of one finished object, you get a sequence of small chunks as the model produces them, so text can appear on screen immediately.

import os
from dotenv import load_dotenv
from openai import OpenAI

load_dotenv()

client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))

stream = client.chat.completions.create(
    model="gpt-4o-mini",
    messages=[{"role": "user", "content": "List three uses for a summarising script."}],
    stream=True,
)

for chunk in stream:
    piece = chunk.choices[0].delta.content
    if piece:
        print(piece, end="", flush=True)

print()  # final newline once the stream ends

Two details make this work. Each chunk carries a delta — the small piece of new text, not the whole reply so far — which is why you print it with end="" and let the pieces run together. And a chunk's delta.content is None at the very start and end of the stream, so the if piece: guard stops your script crashing on the bookend chunks.

The trade-off is bookkeeping. A streamed response has no usage object waiting for you at the end unless you ask for one by passing stream_options={"include_usage": True}, and you cannot inspect the finished text before showing it to a user, which matters if you were planning to parse it as JSON first. Use streaming where a person is watching output arrive; stick with a normal call when your code is the only reader.

Parameter reference

These are the settings you will reach for most. Pass them as keyword arguments to chat.completions.create. Defaults shown are the common OpenAI defaults; other providers are similar but check their docs.

NameTypeDefaultEffect
modelstringnone (required)Which model answers. gpt-4o-mini is cheap and fast; larger models reason better but cost more.
messageslist of dictsnone (required)The conversation. Each item has a role (system, user, or assistant) and content.
temperaturefloat1.0Randomness, from 0.0 to 2.0. Low values give consistent, focused replies; high values give varied, creative ones.
max_tokensintegermodel maxHard ceiling on the reply length in tokens. Set it low while testing to cap costs.
top_pfloat1.0Nucleus sampling, an alternative to temperature. Lower values narrow word choice. Tune one, not both.
ninteger1How many separate replies to generate per call. Each one is billed.
stopstring or listnullText that, when produced, ends the reply early. Useful for fixed formats.
streambooleanfalseWhen true, the reply arrives token by token instead of all at once.
response_formatdictnullSet to {"type": "json_object"} to force valid JSON output.
timeoutfloatSDK defaultSeconds to wait before giving up on a slow request.

Troubleshooting common errors

These are the errors almost everyone hits in their first week. Each gets a dedicated guide if you need the deep version.

Before you read any of them, learn the triage habit: an API error names the part of the request that failed, so the first word of the exception usually tells you where to look. Sorting the failure into one of three families — credentials, size, or the network — narrows six possible causes down to one in a few seconds. If tracebacks themselves still look like noise, Debugging Python AI Errors teaches you how to read one.

A decision tree for triaging a failed API call Starting from a failed request, the error name sorts the problem into a credentials issue, a size issue, or a slow or empty response, each with its own first fix. The call failed read the error name 401 or 429 key or quota Context length request too big Timeout, None slow or empty Check .env, then back off Trim or split the input text Raise timeout check the reason
Sort the exception into one of three families first, then apply that branch's fix — it turns a wall of red text into a two-step decision.
  1. AuthenticationError: Error code: 401 - Incorrect API key provided — Your key is missing, mistyped, or not being loaded. Most often .env was never read, so the variable is empty. Confirm load_dotenv() runs before you create the client and that the key in .env has no quotes or stray spaces. Full walkthrough: Fix the 401 Unauthorized Error in OpenAI Python.
  2. RateLimitError: Error code: 429 - Rate limit reached for requests — You sent calls faster than your tier allows, or you have hit a spending cap. Wait, then retry with an increasing delay (exponential backoff). Step-by-step fix: Fix the 429 Rate-Limit Error in Python.
  3. json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0) — You tried to parse the model's text as JSON, but it wrapped the JSON in prose or code fences. Add response_format={"type": "json_object"} and ask explicitly for JSON. Details: Fix JSONDecodeError with AI API Responses in Python.
  4. BadRequestError: ... maximum context length is N tokens, however you requested M — Your prompt plus requested reply is larger than the model's window. Shorten the input, summarise long documents, or lower max_tokens. How to fix it: Fix the Context-Length-Exceeded Error in Python.
  5. APITimeoutError: Request timed out — The model took longer than your timeout allowed, common with large prompts or long replies. Raise the timeout value (for example timeout=60) and consider streaming so partial output arrives sooner.
  6. AttributeError: 'NoneType' object has no attribute 'content' — You read message.content when the reply was empty, often because the request was filtered or stopped early. Check finish_reason before using the text and handle the empty case instead of assuming a string is always present.

Worked example: a small, safe API client

This script ties everything together. It loads the key safely, sends a request, retries politely when rate-limited, and reports both the reply and the token cost. Save it as ask.py and run it.

import os
import time
from dotenv import load_dotenv
from openai import OpenAI, RateLimitError, APITimeoutError

load_dotenv()  # pulls OPENAI_API_KEY from .env (remember: .env is in .gitignore)

client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"), timeout=30.0)


def ask(prompt: str, max_retries: int = 3) -> str:
    """Send one prompt, retry on rate limits, and return the reply text."""
    messages = [
        {"role": "system", "content": "You are a concise, helpful assistant."},
        {"role": "user", "content": prompt},
    ]
    for attempt in range(max_retries):
        try:
            response = client.chat.completions.create(
                model="gpt-4o-mini",
                messages=messages,
                temperature=0.3,   # low = consistent answers
                max_tokens=200,    # cap the reply to control cost
            )
            usage = response.usage
            print(f"[tokens: {usage.total_tokens} total]")  # keep cost visible
            return response.choices[0].message.content
        except (RateLimitError, APITimeoutError) as error:
            wait = 2 ** attempt + 0.5  # 1.5s, 2.5s, 4.5s — exponential backoff
            print(f"Attempt {attempt + 1} failed ({error.__class__.__name__}); "
                  f"retrying in {wait}s...")
            time.sleep(wait)
    raise RuntimeError(f"Gave up after {max_retries} attempts.")


if __name__ == "__main__":
    answer = ask("Summarise what an LLM API does in two sentences.")
    print(answer)

Run it with python ask.py. You get a clean answer, a one-line token report, and automatic recovery if the service briefly throttles you — the three habits that separate a toy script from one you can trust.

Next steps

You can now call a model, read its reply, tune its behaviour, and recover from the common failures. Here is where to go next, depending on what you want to do.

Back to Python AI Fundamentals for Non-Developers.

Frequently asked questions

What is an LLM API in plain terms?

An LLM API is a web address you send text to and get generated text back from. Your Python script sends a prompt over the internet, a hosted language model writes a reply, and the service returns it as structured data. You never download or run the model yourself.

What is a token and why does it matter?

A token is a small chunk of text, roughly three-quarters of a word in English. Providers count tokens to set both your bill and the size limit of a request. Watching your token counts keeps costs predictable and avoids context-length errors.

What does streaming change about an API response?

With stream=True the reply arrives as many small chunks instead of one finished object, so text can appear on screen while the model is still writing. You print each chunk's delta as it lands. The trade-off is that you cannot inspect or parse the whole answer before showing it.

Is it safe to put my API key in my Python file?

No. Never paste a key directly into code you might share or commit. Store it in a .env file that is listed in .gitignore, then load it at runtime. A leaked key can be used by strangers and billed to you.

Which model should a beginner start with?

Start with a small, cheap model such as gpt-4o-mini. It answers most everyday tasks well, costs very little per call, and lets you experiment freely. Move up to a larger model only when a task clearly needs deeper reasoning.