sethserver.com Subscribe
Two cartoon robots sit at a pink table with coffee cups, working on laptops - one blue robot on the left and one yellow robot on the right. Behind them is a large monitor displaying Python code, with Python logo icons floating around, a blue coffee maker on the left side, and decorative potted plants with spiral stems on both edges of the scene.

Calling LLMs from Python: LangChain, LiteLLM, and HF Transformers

By Seth Black • Updated: September 17, 2026

Python · 4 min read

Like this kind of writing? Get one email a week with notes on startups, AI, and the occasional strong opinion about Python: subscribe to the newsletter.

Calling LLMs from Python: LangChain, LiteLLM, and HF Transformers

Most "getting started" LLM tutorials end right after: call the API and print the response. That's like declaring your startup "enterprise-ready" because you bought a domain and set up a Slack workspace.

That's demo-ready. Not production-ready.

What comes next is the boring stuff that keeps your app alive after the first customer shows up: repeatability, reliability, cost control, and measurability. None of this is glamorous. All of it is what separates "cool prototype" from "system that runs for six months without waking you up at 2am."

We'll stay vendor-agnostic, but I'll name names when it helps.


1) Stop building prompts with string concatenation like it's 1997

If your prompts are built with f"..." plus three + signs, you're one refactor away from chaos. And yes, I've watched three different startups debug the same prompt injection bug because they were doing string concatenation. Don't be startup number four.

Do the boring, grown-up thing:

Use actual templates. Version them. Keep user data away from your instructions so you don't accidentally inject someone's email address into your system prompt.

If you like LangChain, its prompt tools are fine:
https://python.langchain.com/docs/concepts/prompt_templates/

If you don't, a tiny template works too:

from string import Template

# Keep instructions stable. Keep user content clearly separated.
PROMPT = Template(
    "You are a helpful assistant.\n"
    "Return JSON only.\n"
    "Question: $question\n"
)

prompt = PROMPT.substitute(question=user_question)

2) Retries + rate limits: be boring on purpose

APIs fail. Networks wobble. You will get rate-limited. Handle it like an adult. Exponential backoff with jitter is boring, which means it works.

import time
import random
import litellm  # https://github.com/BerriAI/litellm

def call_with_retry(**kwargs):
    # Yes, 6 attempts. Because 5 wasn't enough and 7 is overkill.
    for attempt in range(6):
        try:
            return litellm.completion(**kwargs)
        except Exception as e:
            # Backoff + jitter to avoid synchronized retry storms.
            sleep = (2 ** attempt) + random.random()
            time.sleep(sleep)

    raise RuntimeError("LLM call failed after retries")

LiteLLM is great here because it normalizes providers (OpenAI, Anthropic, etc.) behind one interface. I've used it on three different projects and the API stays consistent even when OpenAI decides to change their response format for the fifth time.


3) Stream responses for better UX

Even if the model takes 8 seconds, streaming makes it feel fast. Users care about "time to first token," not your feelings about latency budgets.

resp = litellm.completion(
    model="gpt-4o-mini",
    messages=[{"role": "user", "content": prompt}],
    stream=True,
)

for event in resp:
    # Sometimes it's content, sometimes it's empty. Streaming is a journey.
    chunk = event["choices"][0]["delta"].get("content", "")
    print(chunk, end="", flush=True)

4) Cache deterministic calls (save money, reduce latency)

If temperature=0 and the input is the same, cache it. This is not "premature optimization." This is "not lighting money on fire."

Keep it simple:
Key by model, prompt, and settings. Store the response and some metadata. Expire it when you change your prompts. This isn't rocket science, it's just being cheap in a smart way.


5) Structured outputs: stop parsing vibes

Use JSON mode or tool/function calling. Free-text parsing is a hobby, not a strategy.

Yes, you can write a regex that parses JSON-ish text 80% of the time. You can also cut your own hair. Neither is a good idea when better options exist.

If you're building more complex integrations, consider when to write an MCP versus a simple tool for your LLM application architecture.


6) Logging without leaking user data

Log enough to debug, not enough to end up in a compliance meeting explaining why customer social security numbers are in your application logs.

The practical version:
- Hash user identifiers (or use stable internal IDs)
- Redact PII (and do it before logs leave the box)
- Store prompts separately from raw user text
- Keep a "golden set" of safe test inputs you can replay anytime


7) Set up evals before you're lost

Nobody does this early enough. You'll spend two weeks tweaking prompts, then realize you have no idea if version 47 is better than version 12. Build the eval harness when you write your first prompt.

You don't need a PhD-grade benchmark. You need consistency:
- A handful of test cases
- Expected traits (correctness, tone, JSON validity)
- A score you can track over time

If you want a framework, look at OpenAI Evals:
https://github.com/openai/evals

Or roll your own. The point is you can ship changes without guessing.


None of this is glamorous. But the difference between a demo and a system that runs in production for six months is exactly this kind of boring infrastructure work. Build it early, while you still have the energy and before your customers become your monitoring system.

If you shipped your MVP quickly and are now dealing with the vibe coding hangover, these patterns will help you stabilize your system. And if you're working with async Python code for concurrent LLM calls, check out these Python asyncio recipes for production-ready patterns.

-Sethers

Share this post
Newsletter

One email, once a week.

Notes on databases, systems, and the occasional strong opinion about Python. No spam, unsubscribe anytime.

Seth Black
Written by

Seth Black

Engineer and founder based in Texas. Writes about databases, AI, and running things in production. Embeds in small teams as lead engineer.

More from Python

View all →
Python

OpenAI Bought Astral - and my fav tool uv

Mar 23, 2026
Python

Python Pydantic Validation: Stop Writing Manual Checks

Mar 04, 2026
Python

Python asyncio Recipes

Mar 04, 2026