~/TechPurAI
~/tutorials/ai-projects-with-python/common-mistakes-building-ai-powered-python-apps
intermediate·part 21 of 22·9 min read

Common Mistakes Building AI-Powered Python Apps

Updated Aug 31, 2026Python · AI

Every part in this series flagged one mistake in its own Callout, as it came up while building ten different applications. This part collects all 14 in one place, each with the code that causes it and the code that fixes it. It's specific to building AI features into a Python web app — not agent design (see Common Mistakes Building AI Agents) or retrieval pipelines (see Common Mistakes with RAG and Embeddings), which are different layers with their own failure patterns.

Mistake 1: hardcoding API keys, or exposing them to a frontend (part 1, part 14)

python
# app.py — committed straight into the file
ANTHROPIC_API_KEY = "sk-ant-api03-abc123..."
javascript
// static/chat.js — ships to every visitor's browser as plain text
const res = await fetch("https://api.anthropic.com/v1/messages", {
  headers: { "x-api-key": "sk-ant-api03-abc123..." },
});
python
# app.py
import os
from dotenv import load_dotenv
load_dotenv()
ANTHROPIC_API_KEY = os.environ["ANTHROPIC_API_KEY"]
javascript
// static/chat.js — calls your own backend, which holds the real key server-side
const res = await fetch("/api/chat", { method: "POST", body: JSON.stringify({ message }) });

Cost: a key that ends up in a public repo's history or a browser's network tab gets extracted and used on your bill within hours of exposure — this is the single most expensive mistake on this list to leave unfixed.

Mistake 2: a global variable holding conversation state in a multi-user app (part 3)

python
conversation_history = []  # module-level — shared by every visitor

@app.route("/chat", methods=["POST"])
def chat():
    conversation_history.append({"role": "user", "content": request.json["message"]})
    reply = call_ai(conversation_history)
    return jsonify({"reply": reply})
python
@app.route("/chat", methods=["POST"])
def chat():
    session_id = request.json["session_id"]
    history = redis_client.get_conversation(session_id)
    history.append({"role": "user", "content": request.json["message"]})
    reply = call_ai(history)
    redis_client.save_conversation(session_id, history)
    return jsonify({"reply": reply})

Cost: with a module-level list, visitor B's message gets appended to the same history visitor A is reading from — one person's conversation genuinely leaks into another's the moment two requests overlap.

Mistake 3: putting AI logic directly inside a Django view (part 4)

python
def chat_view(request):
    client = Anthropic(api_key=settings.ANTHROPIC_API_KEY)
    response = client.messages.create(model="claude-sonnet-5", messages=[...])
    return JsonResponse({"reply": response.content[0].text})
python
# services/ai.py — reusable from a view, a management command, or a Celery task
def get_ai_reply(message: str) -> str:
    client = Anthropic(api_key=settings.ANTHROPIC_API_KEY)
    response = client.messages.create(model="claude-sonnet-5", messages=[{"role": "user", "content": message}])
    return response.content[0].text

# views.py
def chat_view(request):
    return JsonResponse({"reply": get_ai_reply(request.POST["message"])})

Cost: logic locked inside a view can't be called from a management command for a batch job, or from a background task — it gets copy-pasted instead of reused, and the two copies drift apart the first time one gets a bug fix the other doesn't.

Mistake 4: an unauthenticated AI API endpoint (part 7)

python
@app.post("/summarize")
def summarize(request: SummarizeRequest):
    return call_ai(request.text)
python
@app.post("/summarize")
def summarize(request: SummarizeRequest, api_key: str = Depends(verify_api_key)):
    return call_ai(request.text)

Cost: this is genuine, ongoing financial exposure, not a theoretical one — every unauthorized call to an endpoint that wraps a paid LLM API costs real per-token money, and an open endpoint gets found by scanners quickly.

Mistake 5: rate limiting by IP alone for a B2B API (part 7)

python
@limiter.limit("100/hour")  # keyed by request.remote_addr by default
def summarize(): ...
python
@limiter.limit("100/hour", key_func=lambda: request.headers["X-Client-Id"])
def summarize(): ...

Cost: every legitimate client behind the same corporate NAT or cloud provider's outbound IP gets throttled together as if they were one caller — a large customer's whole team hits the ceiling because of traffic from a client they've never heard of.

Mistake 6: a vague summarization or translation instruction (part 8, part 10)

python
system = "Summarize this."
python
system = (
    "Summarize the following support ticket in exactly 2 sentences: "
    "the customer's core issue, and the resolution they're asking for. "
    "Do not include pleasantries or ticket metadata."
)

Cost: a vague instruction produces an inconsistent output shape — sometimes a paragraph, sometimes bullet points, sometimes with a pleasantry attached — which breaks any downstream code expecting a predictable structure.

Mistake 7: sending an entire large document in one request (part 9)

python
response = client.messages.create(
    model="claude-sonnet-5",
    messages=[{"role": "user", "content": entire_200_page_pdf_text}],
)  # fails outright once this exceeds the model's context window
python
chunks = split_into_chunks(document_text, max_tokens=4000)
chunk_summaries = [summarize_chunk(c) for c in chunks]
final_summary = summarize_chunk("\n".join(chunk_summaries))

Cost: this isn't graceful degradation — it's a hard failure the moment a document crosses the context-window limit, and it happens unpredictably depending on which document a user happens to upload.

Mistake 8: trusting AI translation for legally or contractually significant text (part 10)

python
def translate_for_delivery(text: str) -> str:
    return ai_translate(text)  # ships straight out, regardless of what the text is
python
def translate_for_delivery(text: str, is_legally_binding: bool) -> str:
    translation = ai_translate(text)
    if is_legally_binding:
        return queue_for_human_review(translation)
    return translation

Cost: fluent-sounding output is not the same as professionally reviewed output — a mistranslated clause in a contract or terms-of-service document carries real stakes that a marketing blurb doesn't, and the code should treat them differently.

Mistake 9: treating an entire creative-generation task as equally safe to fabricate (part 14)

python
system = "Write a customer testimonial for this product."
python
system = (
    "Write promotional copy for this product. Do not invent specific "
    "customer names, quotes, statistics, or any claim that reads as "
    "factual — write '(NEEDS REAL EXAMPLE)' as a placeholder instead."
)

Cost: an invented testimonial or statistic in generated marketing copy is a specific factual claim, not a creative liberty — publishing it is a real, avoidable legal and trust problem, not a style issue.

Mistake 10: describing brand voice with adjectives instead of real examples (part 15)

python
system = "Write in a friendly, professional, approachable tone."
python
system = (
    "Match the tone of these three real examples of our brand voice:\n"
    f"1. {brand_example_1}\n2. {brand_example_2}\n3. {brand_example_3}"
)

Cost: "friendly, professional, approachable" technically matches almost anything — output that satisfies the adjectives can still not sound genuinely on-brand, because adjectives don't encode the actual sentence rhythm or word choice real examples do.

Mistake 11: fully automating a sent, external communication with no human review (part 16)

python
def send_followup_email(lead):
    body = generate_email(lead)
    send_email(lead.email, body)  # goes out immediately, unreviewed
python
def queue_followup_email(lead):
    body = generate_email(lead)
    draft = EmailDraft.objects.create(lead=lead, body=body, status="pending_review")
    notify_sales_rep(draft)

Cost: an AI-generated email with an incorrect claim or an off tone reaching an external prospect under the company's name is a real reputational risk — a review queue costs one extra click per email and removes it.

Mistake 12: caching a genuinely unique, one-off request like a chatbot turn (part 18)

python
@cache.memoize(timeout=3600)
def chat_response(user_message: str) -> str:
    return call_ai(user_message)  # near-zero cache hit rate — chat turns rarely repeat verbatim
python
@cache.memoize(timeout=3600)
def answer_faq(question_key: str) -> str:  # a normalized, genuinely repeated key
    return call_ai(FAQ_PROMPTS[question_key])

Cost: caching adds real complexity — a cache key strategy, invalidation, memory overhead — for zero benefit when the input essentially never repeats. It earns its cost on genuinely repeated inputs like FAQ lookups, not free-form chat.

Mistake 13: a test suite that calls the real API on every run (part 19)

python
def test_summarize_endpoint():
    response = client.post("/summarize", json={"text": "some text"})
    assert response.status_code == 200  # hits the real Anthropic API every CI run
python
def test_summarize_endpoint(mock_ai_client):
    mock_ai_client.messages.create.return_value = FAKE_SUMMARY_RESPONSE
    response = client.post("/summarize", json={"text": "some text"})
    assert response.status_code == 200

Cost: a real-API test suite is slow, costs real money on every run, and fails on transient network issues that have nothing to do with an actual code bug — exactly the kind of flakiness that trains a team to ignore failing CI. Part 19 covers mocking this properly.

Mistake 14: deploying a .env file instead of platform-managed secrets (part 20)

dockerfile
COPY .env .env
python
# read from the platform's injected environment at runtime — nothing
# secret is ever copied into the image
ANTHROPIC_API_KEY = os.environ["ANTHROPIC_API_KEY"]
text
Fly.io:  fly secrets set ANTHROPIC_API_KEY=...
Render:  set in the dashboard's Environment tab
Vercel:  vercel env add ANTHROPIC_API_KEY

Cost: COPY .env .env bakes the secret into an image layer that persists in the image's history even if a later layer deletes the file — anyone with pull access to that image can extract it. Part 20 covers platform-managed secrets directly.

The thread connecting all fourteen

Nearly every mistake above comes from treating an AI-powered feature like ordinary application code in the one dimension where it genuinely isn't — per-request cost, non-determinism, and higher-stakes external-facing output — while everything else about it (structure, testing, security) follows completely ordinary software engineering discipline.

Common mistake

Assuming AI-specific concerns replace normal software engineering discipline rather than adding to it. Every project in this series still needed ordinary error handling, testing, and security — the AI-specific concerns (grounding, cost, non-determinism) are additions on top of that foundation, not a replacement for it.

FAQ

Which of these is the most expensive to leave unfixed? Mistake 1 (exposed API keys) and Mistake 4 (unauthenticated endpoints) are the two with unbounded financial exposure — both are worth checking before anything else on this list, since either one turns into a real bill within hours of being found.

Do these apply outside of Python specifically? The failure patterns themselves — global state, unauthenticated endpoints, vague instructions — aren't Python-specific. The code examples are Python because that's what this series builds in, but the same 14 categories apply in any language.

How is this different from the AI agents mistakes page? This page covers building AI features into a conventional web app — chatbots, summarizers, translators, content generators. Common Mistakes Building AI Agents covers a different layer: autonomous tool-use loops, multi-step planning, and agent-specific memory and security concerns. A Django app that calls Claude once per request and returns the result is this page's territory, not that one's.

Is caching AI responses ever a good idea? Yes — for genuinely repeated inputs like FAQ answers or a fixed set of report summaries (see Mistake 12 and part 18). The mistake is applying it to inputs that vary too much for a cache hit to ever happen.

Do I need to mock the AI API in every test? For unit and integration tests that run on every commit, yes — see part 19. A small, separate suite of real-API smoke tests run on a schedule (not every CI run) is a reasonable way to still catch real API changes.

Next, and last: the capstone — combining several of this series' tools into one AI toolkit for Bright Leaf Coffee and GreenDesk.

VK

Vijay Kumar

Founder of TechPurAI — writing hands-on tutorials and honest tool breakdowns.

LinkedIn ↗
← previous20. Deploying a Real Python AI Applicationnext →22. The Capstone: A Real AI Toolkit for Bright Leaf Coffee and GreenDesk