Stop Rewriting Code You Already Wrote
Most Python projects I look at have the same structural rot. It isn't from bad logic. It is from people copying patterns they saw in tutorials and never questioning whether they actually apply to what they are building. I spent three weeks last year debugging a data pipeline that failed on one specific edge case, and the root cause was a list mutation issue that had been hiding in plain sight for months. The most destructive mistake beginners make is modifying a list while iterating over it. You write a loop that removes items based on a condition, the indices shift under you, and half the items you expected to process get silently skipped. This happens constantly in production code because the error doesn't throw an exception. The loop just completes with wrong results. The workaround is to iterate over a copy of the list or build a new list instead of mutating the original. A list comprehension is usually cleaner: [x for x in original if not should_exclude(x)]. It does not look dramatic. It prevents the index drift problem entirely and makes the intent obvious to anyone reading the code later.
Another mistake that comes up repeatedly is assuming dictionaries preserve insertion order the way people expect. In Python 3.7 and later they do, but the guarantee only applies to CPython as an implementation detail until Python 3.8 made it official. If you write code that depends on key order and your runtime environment switches versions or implementations, you can get subtle ordering bugs that appear out of nowhere. I ran into this when a colleague upgraded a service from 3.7 to 3.11 and an API response field order changed, which broke a downstream consumer that compared string representations directly. The fix was using an OrderedDict or relying on explicit key ordering rather than position. Mutation of mutable default arguments is the third mistake that wastes the most time. People write functions like def add_item(item, lst=[]) and expect a fresh list every call. They get a single shared list that accumulates across calls. This behavior is documented, but almost no one reads the documentation before writing it. I fixed a production issue once where a caching layer was accumulating keys across unrelated requests because a decorator used a mutable list as a default. Changing the signature to lst=None and initializing inside the function body resolved it immediately.
Scope And Closure Traps
Closures capture variables by reference, not by value. When you create functions inside a loop and each one references the loop variable, they all end up pointing to the same variable after the loop finishes. This produces results that look completely impossible at first glance. I encountered this in a script that generated multiple database queries dynamically. Each query function was supposed to use its own parameter, but they all used the final value of the loop variable. The queries were identical and returned duplicate data. The fix was binding the variable at definition time using a default argument: def make_query(param=i). That locks the value into each closure at creation time rather than deferring the lookup. This same reference-capture problem shows up in threading and async code. A background task that captures a loop variable may process the wrong item if the loop has already moved on. The solution is the same: bind the value at definition time.
Get the Full Details

Exception Handling That Hides Problems
Using bare except: clauses is one of the fastest ways to lose track of what is actually broken. It catches everything including KeyboardInterrupt and , which means pressing Ctrl+C to stop a script might silently exit or behave unexpectedly depending on your Python version and context. Always catch specific exceptions. If you need to catch a broad range, catch Exception explicitly so you do not intercept system-level signals. I once spent two days trying to debug why a daemon kept exiting without any error output. The issue was a bare except clause swallowing a signal handler exception during shutdown. Adding proper exception handling and logging made the real problem visible immediately. Another common pattern is catching an exception just to re-raise it or print it. This adds overhead and hides the real error from tracebacks. Either handle it meaningfully or do not catch it at all.
List Comprehension Scope Leaks
List comprehensions in Python create a separate scope, but generator expressions and dict comprehensions behave differently depending on the Python version. In Python 2, list comprehensions leaked their loop variables into the enclosing scope. In Python 3 this was fixed, but people migrating old code sometimes get burned by this difference. If you write a list comprehension and later reference the loop variable outside the comprehension, it may work in one version and fail in another. I learned this the hard way when porting a legacy script from Python 2 to 3. A variable I thought was scoped to the comprehension was still accessible afterward in the 2.x environment, and removing that dependency required a full audit of the codebase. The workaround is to assume loop variables from comprehensions are local and never reference them after the comprehension ends.
Thread Safety And GIL Assumptions
Python's Global Interpreter Lock means threads do not run Python bytecode in parallel. This is frequently misunderstood. People write multi-threaded code assuming CPU-bound tasks will speed up, and they do not. The GIL serializes execution of Python bytecode, so CPU-bound threading often performs worse than a single thread due to context switching overhead. For I/O-bound tasks, threading is fine. For CPU-bound work, use multiprocessing or concurrent.futures.ProcessPoolExecutor. I once optimized a data processing task by switching from ThreadPoolExecutor to ProcessPoolExecutor, and the wall-clock time dropped from about 90 seconds to roughly 20 seconds on an 8-core machine. The code itself barely changed. Shared mutable state between threads requires locks. Using a lock incorrectly, such as acquiring it inside a context that already holds it, causes deadlocks. I wrote a module that used a recursive lock (RLock) to prevent this, but then spent hours debugging why a certain operation would never complete because the lock was never released in an error path. The fix was using a try-finally block around every lock acquisition.

Import Side Effects
Code at module level runs when the module is imported. This is obvious, but people often put expensive operations or side effects at the top level without realizing it. A module that makes network calls or writes files during import will do so every time anything imports it, even if those operations are never needed in the current execution path. I found a project where a utility module contained a startup routine that checked for updates and logged metrics. Every test that imported the module triggered the network call. Test suites ran slowly and failed intermittently because the external service was unavailable. Moving the logic into an explicit main() function or an initialization method that gets called deliberately fixed both problems. The same issue applies to large data structures initialized at import time. Loading a 50 MB dictionary at module level increases import time significantly and consumes memory even when the data is not used. Build the structure lazily inside a function instead.
String Formatting Performance
f-strings are faster than .format() and % formatting in CPython because the interpolation happens at the C level during compile time rather than at runtime. The difference is small for one-off formatting but measurable in tight loops. I benchmarked a loop that formatted 1 million strings using each method, and f-strings were roughly 30 to 40 percent faster than .format(). For SQL queries and template strings, string concatenation is slower and more error-prone than using proper parameterized queries or templates. I fixed a query builder that used string concatenation to assemble SQL, which had both performance issues and a security vulnerability. Switching to parameterized queries with psycopg2's %s placeholders eliminated both problems at once.
Assuming Immutability
Tuples are immutable, but the objects they contain may not be. A tuple containing a list is not truly immutable because the list inside can be modified. This creates confusion when tuples are used as dictionary keys or set members under the assumption that their contents cannot change. I encountered this in a caching layer that used tuples containing lists as cache keys. The lists were mutated after insertion, which changed the hash and made the cache entries unreachable. The fix was converting the inner lists to tuples so the entire key was genuinely immutable. Classes that override __hash__ without overriding __eq__ cause similar issues. If two objects compare equal but have different hashes, they may be stored in separate buckets in a set or dict. Python will not find them when looking them up. Always implement __eq__ whenever you implement __hash__.

Async/Await Misuse
Async code in Python requires careful handling of blocking operations. Calling a synchronous function inside an async context blocks the entire event loop. I once wrote an async web scraper that used the standard requests library instead of aiohttp, and the entire application became effectively synchronous because every network call blocked the loop. Using asyncio.to_thread() or wrapping blocking calls properly allows the event loop to continue processing other tasks while the blocking work runs in a thread pool. The performance difference was dramatic: the scraper throughput went from about 2 requests per second to roughly 50 requests per second after the fix. Another async mistake is forgetting to await coroutines. Writing async_function() without await creates a coroutine object that is never executed. No error is raised. The code just silently skips the operation. I debugged this exact issue in a background task scheduler where scheduled jobs were never running. Adding await to every coroutine call fixed it immediately.