Common Python errors and fixes: best practices explained. Errors every developer hits — and how to fix them.
After 80 days of building Python backends, you've probably hit errors that seemed cryptic at first but made sense once diagnosed. We're going to walk through four errors that appear repeatedly across production systems and show you exactly how to fix them—and more importantly, how to avoid them in the future.
ModuleNotFoundError: wrong venv or path
This is often the first error new developers encounter, but even experienced teams hit it when onboarding or switching between projects. The error means Python can't find a module you're trying to import, usually because your virtual environment isn't activated or your Python path doesn't include the module location.
The scenario: you've just cloned a repository, installed dependencies, and try to run the application. You get ModuleNotFoundError: No module named 'fastapi' even though it's in requirements.txt. Nine times out of ten, you're running Python from the system interpreter instead of the venv.
# Wrong approach - using system Python
$ python main.py
ModuleNotFoundError: No module named 'fastapi'
# Correct approach - activate venv first
$ source venv/bin/activate # macOS/Linux
$ venv\Scripts\activate # Windows
$ python main.py # Now worksBut there's a deeper issue: your IDE or editor might be configured to use the wrong interpreter. We've seen this happen when switching between projects—PyCharm, VS Code, or your editor still points to the previous project's venv or the system Python.
Check your interpreter path explicitly. In a script, you can verify which Python is running:
import sys
print(sys.executable) # Should show your venv path
print(sys.path) # Shows all search pathsIf you're building a package or working with relative imports, verify your package structure includes __init__.py files in directories you want to import from. A missing __init__.py can silently break imports across your codebase.
RecursionError: missing base case
Recursion is elegant when it works and catastrophic when it doesn't. A RecursionError happens when a function keeps calling itself without a condition to stop, hitting Python's recursion limit (usually around 1,000 calls by default).
Let's say you're building a tree traversal for an organizational hierarchy. You write recursive code to walk up the chain to find the root manager:
# Wrong - missing base case
def find_root_manager(employee_id):
employee = db.get_employee(employee_id)
return find_root_manager(employee.manager_id) # No stop condition!
# Correct - with base case
def find_root_manager(employee_id):
employee = db.get_employee(employee_id)
if employee.manager_id is None: # Base case
return employee
return find_root_manager(employee.manager_id)The base case is your escape hatch. Without it, recursion never stops. In the example above, if you have circular manager references or a data issue, you'd hit RecursionError quickly.
There's a second, subtler issue: Python's recursion limit is shallow compared to other languages. If you're processing deeply nested structures (like a category tree 500 levels deep), you'll hit the limit. In those cases, convert to iteration or increase the limit carefully:
import sys
sys.setrecursionlimit(5000) # Be cautious—can cause stack overflowPrefer iteration for performance-critical code. Recursion is cleaner conceptually but costs more in memory and CPU per call. In a request handler processing thousands of items, that overhead adds up.
UnboundLocalError: assigning a global inside a function
This error catches developers off guard because the code looks correct at first glance. You're reading a global variable, modifying it, and Python interprets that as creating a local variable—but you're trying to read it before assignment, causing UnboundLocalError.
A realistic scenario: you have a shared counter or cache dictionary at the module level, and a function tries to update it:
counter = 0
def increment_counter():
counter += 1 # UnboundLocalError: local variable 'counter' referenced before assignment
return counter
increment_counter()Python sees counter += 1 and thinks "this function creates a local counter." But the right-hand side of += reads counter before it's been assigned locally, hence the error.
The fix is to declare the variable as global:
counter = 0
def increment_counter():
global counter
counter += 1
return counter
increment_counter() # WorksHowever, we rarely recommend global state in production code. It's error-prone and hard to test. A better approach is to use a class or pass the value as an argument:
# Better approach with a class
class Counter:
def __init__(self):
self.value = 0
def increment(self):
self.value += 1
return self.value
counter = Counter()
counter.increment() # Clean, testable, no global stateIf you're working with mutable objects like dictionaries or lists, you can modify them without the global keyword because you're not reassigning the variable itself—just mutating its contents:
cache = {}
def set_cache(key, value):
cache[key] = value # No global needed—we're mutating the dict, not reassigning it
set_cache('user_123', {'name': 'Alice'}) # Works fineEvent loop is closed: mixing sync and async incorrectly
As you move into async programming with frameworks like FastAPI or aiohttp, this error becomes common. It typically means you're trying to run an event loop when one is already closed, often by mixing synchronous and asynchronous code carelessly.
A typical scenario: you're using an async library (like an async database driver) but accidentally running it in a sync context, or you're trying to run a second event loop in the same thread:
import asyncio
async def fetch_data():
await asyncio.sleep(1)
return "data"
# Wrong - trying to run event loop multiple times
asyncio.run(fetch_data())
asyncio.run(fetch_data()) # RuntimeError: Event loop is closedEach call to asyncio.run() creates and closes an event loop. If you need to run multiple async functions, structure them together:
# Correct - single event loop for multiple operations
async def main():
result1 = await fetch_data()
result2 = await fetch_data()
return result1, result2
asyncio.run(main())Another common mistake: calling async functions from sync code without proper handling. If you're in a FastAPI endpoint, you're already inside an async context, so you can await directly. But if you're in a CLI script or a Celery task (which is sync), you need to create an event loop:
# In FastAPI (async context already exists)
@app.get("/data")
async def get_data():
data = await fetch_data() # Works - we're in async context
return data
# In a sync CLI script
if __name__ == "__main__":
data = asyncio.run(fetch_data()) # Correct way to bridge sync to async
print(data)If you're debugging this in an IPython or Jupyter notebook, you might hit event loop issues because notebooks have their own event loop. Use nest_asyncio as a workaround for development:
import nest_asyncio
nest_asyncio.apply() # Allows nested event loops in notebooksIn production, never mix sync and async carelessly. If your database driver is async, use it consistently. If you're calling a sync library from async code, use asyncio.to_thread() to avoid blocking the event loop:
import asyncio
def blocking_operation():
# Simulates a sync library call
return "result"
async def main():
# Run blocking operation in a thread pool
result = await asyncio.to_thread(blocking_operation)
return result
asyncio.run(main())Common mistakes
- Forgetting to activate your venv before running code or installing packages. This leads to ModuleNotFoundError and version conflicts across your team.
- Writing recursive functions without testing with edge cases. A missing base case works fine until production data triggers infinite recursion.
- Using global state instead of proper dependency injection or classes. It works at small scale but becomes a debugging nightmare in larger systems.
- Assuming async/await works everywhere. Mixing sync and async contexts without understanding event loops causes hard-to-diagnose bugs in production.
- Not checking Python version and library compatibility. A module import works on your machine but fails on the server running a different Python version.
Key takeaways
- Always verify you're using the correct venv and Python interpreter—check
sys.executableif unsure. - Every recursive function needs a clear base case and should handle edge cases like circular references or deep nesting.
- Avoid global state; use classes, dependency injection, or function arguments to pass values cleanly and make code testable.
- Understand async/await and event loops before mixing sync and async code—use
asyncio.run()to bridge from sync to async, andasyncio.to_thread()to call sync code from async. - Common Python errors and fixes follow patterns—recognizing them early saves debugging time across development, testing, and production environments.






Comments
Loading comments…