Skip to content

Common coding mistakes (and how to fix them)

Beginner-friendly · 15 min · every example runs

Most bugs aren't exotic — they're the same handful of mistakes, made again and again. Each one below shows ❌ the mistake (with the wrong result it really produces) and ✅ the fix.

Jump to: Python gotchas · Error handling · Performance · GenAI-specific · Security


Python gotchas

1. Using a list or dict as a default argument

The default value is created once, when the function is defined — so every call shares it.

def add_message(text, history=[]):
    history.append(text)
    return history

print(add_message("hi"))       # → ['hi']
print(add_message("hello"))    # → ['hi', 'hello']  (old message kept!)
def add_message(text, history=None):
    history = [] if history is None else history   # a fresh list on every call
    history.append(text)
    return history

print(add_message("hi"))       # → ['hi']
print(add_message("hello"))    # → ['hello']

2. Changing a list while looping over it

Removing items shifts the rest left, so the loop skips the next item.

scores = [0.2, 0.9, 0.1, 0.05]
for s in scores:
    if s < 0.5:
        scores.remove(s)
print(scores)                  # → [0.9, 0.05]   0.05 should have been removed too
scores = [0.2, 0.9, 0.1, 0.05]
scores = [s for s in scores if s >= 0.5]       # build a new list instead
print(scores)                  # → [0.9]

3. "Copying" a list with =

= gives the same list a second name — changing one changes both.

base_messages = [{"role": "system", "content": "Be brief."}]
chat = base_messages                    # NOT a copy
chat.append({"role": "user", "content": "Hi"})
print(len(base_messages))      # → 2   the template was changed too
import copy

base_messages = [{"role": "system", "content": "Be brief."}]
chat = copy.deepcopy(base_messages)      # independent copy (also copies the dicts inside)
chat.append({"role": "user", "content": "Hi"})
print(len(base_messages))      # → 1

For a flat list of numbers or strings, items.copy() is enough.

4. Treating 0, "" or [] as "missing"

if not value is also true for 0, empty strings and empty lists — not just None.

def get_top_k(top_k=None):
    if not top_k:              # 0 is "falsy", so it's replaced too
        top_k = 5
    return top_k

print(get_top_k(0))            # → 5   the caller asked for 0
def get_top_k(top_k=None):
    if top_k is None:          # only replace a value that really is missing
        top_k = 5
    return top_k

print(get_top_k(0))            # → 0

5. Naming a variable list, dict, id, input or type

That hides Python's built-in of the same name for the rest of the file.

list = ["rag", "agents"]       # hides the built-in list()
try:
    letters = list("abc")
except TypeError as err:
    print("TypeError:", err)   # → TypeError: 'list' object is not callable
del list                       # (restores the built-in for the next examples)
topics = ["rag", "agents"]     # a descriptive name — and list() still works
print(list("abc"))             # → ['a', 'b', 'c']

6. Functions created in a loop all "remember" the last value

A function looks up loop variables when it runs, not when it's created.

handlers = [lambda: i for i in range(3)]
print([h() for h in handlers]) # → [2, 2, 2]
handlers = [lambda i=i: i for i in range(3)]   # capture the current value as a default
print([h() for h in handlers]) # → [0, 1, 2]

7. Comparing decimals with ==

Computers store most decimals approximately.

print(0.1 + 0.2 == 0.3)        # → False
import math
print(math.isclose(0.1 + 0.2, 0.3))   # → True

Error handling

8. Catching everything and hiding it

A bare except: (or except Exception: pass) hides real bugs — typos, wrong keys, network outages — and leaves you guessing.

import json

reply = "Sure! Here's the JSON you asked for."
try:
    data = json.loads(reply)
except:                        # catches *everything*, says nothing
    data = {}
print(data)                    # → {}   …and nobody knows why
import json
import logging

reply = "Sure! Here's the JSON you asked for."
try:
    data = json.loads(reply)
except json.JSONDecodeError as err:            # only the error you expect
    logging.warning("Model did not return JSON (%s): %.40r", err, reply)
    data = {}
print(data)                    # → {}   and the log says why

9. Opening files without encoding="utf-8"

On Windows the default encoding isn't UTF-8, so files with ₹, emoji or Hindi text break — sometimes only on a colleague's machine.

from pathlib import Path
Path("notes.txt").write_text("Price: ₹900")        # encoding depends on the computer
# On Windows → UnicodeEncodeError: 'charmap' codec can't encode character '₹'
from pathlib import Path
Path("notes.txt").write_text("Price: ₹900", encoding="utf-8")
print(Path("notes.txt").read_text(encoding="utf-8"))   # → Price: ₹900

Performance

10. Building a long string with += in a loop

Each += creates a whole new string — slow for big documents.

chunks = ["refunds within 30 days", "free shipping over ₹500"]
context = ""
for i, chunk in enumerate(chunks, start=1):
    context += f"[{i}] {chunk}\n"
chunks = ["refunds within 30 days", "free shipping over ₹500"]
context = "\n".join(f"[{i}] {chunk}" for i, chunk in enumerate(chunks, start=1))
print(context.splitlines()[0])            # → [1] refunds within 30 days

11. Checking membership in a big list

x in some_list scans every item; x in some_set is near-instant.

seen_ids = []
for doc_id in ["a", "b", "a"]:
    if doc_id not in seen_ids:     # slower and slower as the list grows
        seen_ids.append(doc_id)
seen_ids = set()
for doc_id in ["a", "b", "a"]:
    seen_ids.add(doc_id)           # duplicates are ignored automatically
print(sorted(seen_ids))            # → ['a', 'b']

GenAI-specific

12. Trusting the model's JSON without checking it

Models sometimes add text around the JSON, miss a field, or use the wrong type.

import json

reply = '{"priority": "high"}'                 # the model was asked for a number 1-5
priority = json.loads(reply)["priority"]
print(priority + 1 if isinstance(priority, int) else "crashes later: " + priority)
# → crashes later: high
from pydantic import BaseModel, Field, ValidationError

class Ticket(BaseModel):
    priority: int = Field(ge=1, le=5)          # the shape you expect, enforced

try:
    ticket = Ticket.model_validate_json('{"priority": "high"}')
except ValidationError as err:
    print("Invalid model output:", err.error_count(), "problem")   # → Invalid model output: 1 problem
    # …retry the request, including the error message so the model can fix it

More in Type hints, dataclasses & Pydantic.

13. No timeout or retries on API calls

Networks fail and APIs rate-limit. Without a timeout, one hung request freezes your app.

from openai import OpenAI
client = OpenAI(api_key="sk-test")             # library defaults: you haven't decided
from openai import OpenAI

client = OpenAI(
    api_key="sk-test",
    timeout=30,          # give up on a request after 30 seconds
    max_retries=3,       # retry rate limits and temporary errors, with back-off
)
print(client.timeout, client.max_retries)      # → 30 3

14. Blocking the event loop in async code

time.sleep() or requests.get() inside async def freezes every other task.

import asyncio
import time

async def call_model():
    time.sleep(0.1)          # blocks everything else while it waits
    return "reply"
import asyncio

async def call_model():
    await asyncio.sleep(0.1)  # lets other tasks run while this one waits
    return "reply"

print(asyncio.run(call_model()))   # → reply

Use async libraries inside async def — httpx.AsyncClient, AsyncOpenAI. See Async programming.

15. Sending far too much text to the model

Pasting whole documents into every prompt is slow and expensive — and often gives worse answers, because the important part gets lost.

  • ✅ Retrieve only the relevant chunks (that's what RAG is for).
  • ✅ Summarise long chat histories instead of resending everything.
  • ✅ Log token counts per request so you notice when a prompt suddenly grows.

Security

16. Putting API keys in your code

Keys in code end up in Git history, screenshots and shared notebooks — and leaked keys get abused within minutes.

API_KEY = "sk-proj-1234567890abcdef"           # will leak sooner or later
import os

api_key = os.getenv("OPENAI_API_KEY")          # set in .env or the environment, never in code
if api_key is None:
    print("Set OPENAI_API_KEY first")

Add .env to .gitignore. If a key ever lands in a repository, revoke it immediately — deleting the commit isn't enough.

Also: don't log secrets or personal data

Logging full prompts can store customers' names, emails or keys in plain text. Log IDs, sizes and timings instead — or redact before logging.


A debugging checklist

When something's wrong, check these first:

  • Read the whole error message — the last line says what, the lines above say where.
  • Print (or log) the actual value and its type() just before the failing line.
  • Is a value None when you expected something?
  • Did a list or dict change somewhere you didn't expect (mistakes 1–3)?
  • Are you in the right virtual environment (which python / where python)?
  • For LLM bugs: log the exact prompt and the raw reply.

Next: Write clean, readable code →