📌Free Shipping for all orders over $60. Just add merch to cart. Applied at checkout.

Bad Python code is everywhere. You’ve seen it. Maybe you’ve written it. That’s fine—everyone has. But if you want to level up, you need to stop making the same rookie mistakes over and over.

The difference between a mediocre dev and a great one? Knowing where the pitfalls are and dodging them without thinking. Here are 25 of the most common Python mistakes and how to avoid them.

Or…you’re just here for the solution. Quickly jump to what you’re looking for with the table of contents.


Python errors be like…

1. Syntax & Indentation Errors

The Mistake

Python is strict about indentation, but people still mess it up—mixing tabs and spaces, forgetting colons at the end of if, for, and while statements, or just misaligning blocks entirely. Unlike other languages that might let it slide, Python will straight-up refuse to run your code if you get this wrong.

How to Avoid It

  • Stick to spaces. PEP 8 recommends four spaces per indentation level. Just set your editor to convert tabs to spaces and never think about it again.
  • Use a linter. flake8 or pylint will catch indentation issues before they break your code.
  • Turn on your editor’s “show whitespace” feature. It makes hidden issues (like a sneaky tab in a sea of spaces) obvious.
  • Don’t forget colons (:). If you’re getting unexpected indentation errors, check that your if, for, while, and function definitions actually end with a colon.

Bad Code:

pythonCopyEditif True
    print("This will fail")  # Missing colon
pythonCopyEditdef bad_indent():
  print("This might look fine") 
    print("But it's actually misaligned")  # IndentationError

Good Code:

pythonCopyEditif True:
    print("This works")  # Proper colon usage
pythonCopyEditdef good_indent():
    print("Everything lines up perfectly")  # No mixed indentation

Python isn’t flexible with indentation, so don’t fight it—just set up your tools correctly and move on.


2. Misusing Mutable Default Arguments

The Mistake

Python’s default function arguments don’t work the way most people expect. If you use a mutable type (like a list or dictionary) as a default argument, Python won’t create a new one each time the function is called. Instead, it reuses the same object across multiple calls, leading to unpredictable behavior.

How to Avoid It

Always use None as the default value for mutable arguments and initialize them inside the function. This ensures a fresh object is created each time. If you don’t do this, you’ll end up debugging weird, unintended side effects that make no sense.

Bad Code:

def append_item(item, my_list=[]):
    my_list.append(item)
    return my_list

print(append_item(1))  # [1]
print(append_item(2))  # [1, 2] (not [2] as expected)

Good Code:

def append_item(item, my_list=None):
    if my_list is None:
        my_list = []
    my_list.append(item)
    return my_list

print(append_item(1))  # [1]
print(append_item(2))  # [2] (fresh list each time)

Python isn’t going to warn you about this—it just assumes you know what you’re doing. If you don’t want your default arguments behaving like hidden global variables, don’t use mutable defaults.


3. Forgetting to Close Files

The Mistake

Opening a file without explicitly closing it is a rookie mistake that can lead to resource leaks and unexpected behavior. If you don’t close a file properly, you risk data loss, locked files, or hitting system limits on open files. Python usually cleans up after you, but relying on that is sloppy.

How to Avoid It

Always close files when you’re done with them. The best way? Use the with open() statement. It automatically closes the file when you’re done, even if an error occurs. If you’re manually calling open(), make sure to close it explicitly with file.close().

Bad Code:

file = open("data.txt", "r")
content = file.read()
# Forgot to close the file

Good Code:

with open("data.txt", "r") as file:
    content = file.read()  # File auto-closes when the block ends

Manually managing file resources is outdated. with open() exists for a reason—use it.


4. Using “is” Instead of “==” for Comparison

The Mistake

Python’s is operator checks for object identity, not equality. That means is only returns True if both variables point to the same object in memory, not just if they have the same value. If you mistakenly use is instead of ==, you’ll get unpredictable results—especially with numbers, strings, and lists.

How to Avoid It

Use == when comparing values. Use is only when you’re explicitly checking whether two variables refer to the exact same object (like checking for None).

Bad Code:

a = 256
b = 256
print(a is b)  # True (works due to Python’s caching for small integers)

a = 300
b = 300
print(a is b)  # False (fails because 300 isn’t cached)
my_list = [1, 2, 3]
your_list = [1, 2, 3]
print(my_list is your_list)  # False (they have the same content but are different objects)

Good Code:

a = 300
b = 300
print(a == b)  # True (correct way to compare values)

# Proper use of 'is'
x = None
if x is None:
    print("x is None")  # Right way to check for None

Using is incorrectly leads to some of the hardest-to-debug issues in Python. Stick to == unless you’re deliberately checking if two variables reference the same object.


5. Modifying a List While Iterating

The Mistake

Looping over a list while modifying it is a classic way to introduce unexpected bugs. When you remove or add elements inside a loop, Python shifts the remaining items, which can cause elements to be skipped or processed incorrectly.

How to Avoid It

Instead of modifying the list directly, iterate over a copy of the list or use list comprehensions to generate a new list. If you need to remove elements, use filter() or create a new list with only the elements you want.

Bad Code:

numbers = [1, 2, 3, 4, 5]
for num in numbers:
    if num % 2 == 0:
        numbers.remove(num)  # Skips elements because the list shifts
print(numbers)  # [1, 3, 5] (Expected: [1, 3, 5], but could be inconsistent)

Good Code:

# Iterate over a copy to safely modify the original list
numbers = [1, 2, 3, 4, 5]
for num in numbers[:]:  # Creates a copy
    if num % 2 == 0:
        numbers.remove(num)
print(numbers)  # [1, 3, 5]
# Use list comprehensions for filtering
numbers = [1, 2, 3, 4, 5]
numbers = [num for num in numbers if num % 2 != 0]
print(numbers)  # [1, 3, 5]

Modifying a list while iterating over it is a guaranteed way to introduce subtle, hard-to-catch bugs. Either iterate over a copy or use list comprehensions—it’s cleaner and safer.


6. Ignoring Exception Handling

The Mistake

Skipping exception handling is asking for trouble. If your script crashes because of an uncaught error, it’s game over—no logging, no graceful recovery, just a stack trace. This is especially bad in production environments where a single failure can break an entire system.

How to Avoid It

Wrap risky code in try-except blocks, but don’t just catch everything blindly. Handle specific exceptions, log errors, and fail gracefully when necessary.

Bad Code:

# Will crash if the file doesn’t exist
file = open("non_existent_file.txt", "r")
content = file.read()

Good Code:

try:
    with open("non_existent_file.txt", "r") as file:
        content = file.read()
except FileNotFoundError:
    print("File not found. Please check the filename.")
import logging

# Set up logging to capture exceptions
logging.basicConfig(level=logging.ERROR)

try:
    x = 1 / 0
except ZeroDivisionError as e:
    logging.error(f"Math error: {e}")

Exception handling isn’t just about stopping crashes—it’s about making sure your code fails in a controlled, predictable way. Handle errors, log them, and don’t let bad data or unexpected issues take down your whole application.


7. Catching Too Broad Exceptions

The Mistake

Catching every exception with a blanket except: or except Exception: is lazy and dangerous. It silences all errors, including ones you didn’t expect, making debugging a nightmare. Worse, it can hide real issues like KeyboardInterrupt, preventing the program from stopping when the user wants to exit.

How to Avoid It

Catch only the exceptions you expect and can handle properly. If you really need a broad exception handler (rare cases), log the error instead of failing silently.

Bad Code:

try:
    result = 1 / 0
except Exception:  # Too broad, hides all errors
    print("Something went wrong")  # Not helpful
try:
    risky_operation()
except:  # Catches everything, including KeyboardInterrupt
    pass  # Silently ignores errors (bad idea)

Good Code:

try:
    result = 1 / 0
except ZeroDivisionError:
    print("You can’t divide by zero.")  # Only catching expected errors
import logging

try:
    risky_operation()
except (ValueError, KeyError) as e:  # Catch only what you expect
    logging.error(f"Handled error: {e}")
except Exception as e:  
    logging.critical(f"Unexpected error: {e}", exc_info=True)  # Logs full traceback
    raise  # Still re-raises the exception

Catching everything is a quick way to bury real problems. Be precise with exception handling—your future self will thank you when debugging.


8. Forgetting to Use Virtual Environments

The Mistake

Installing Python packages globally without a virtual environment is a fast track to dependency hell. One project needs Django 4.2, another requires Django 3.2, and suddenly, your system is a mess. Worse, a global package update can break your existing projects.

How to Avoid It

Always use a virtual environment (venv or pipenv) for every project. This isolates dependencies, preventing conflicts and making it easy to reproduce your setup.

Bad Code:

pip install django  # Installs globally, affecting all projects

Good Code:

# Create a virtual environment
python -m venv my_project_env
source my_project_env/bin/activate  # On Mac/Linux
my_project_env\Scripts\activate     # On Windows

# Now install dependencies inside the virtual environment
pip install django
# Alternative: Use pipenv (automates dependency management)
pip install pipenv
pipenv install django

Virtual environments should be non-negotiable. They keep your projects clean, avoid dependency nightmares, and let you manage different Python versions per project. If you’re not using one, you’re doing it wrong.


9. Overusing Global Variables

The Mistake

Using global variables everywhere makes your code unpredictable and hard to debug. Since any function can modify them, tracking down bugs becomes a guessing game. Globals also make unit testing a nightmare because functions depend on hidden state.

How to Avoid It

Keep variables local whenever possible. If you need shared state, pass it explicitly as a function argument or encapsulate it in a class. If you must use a global variable, make it read-only or use a singleton pattern.

Bad Code:

count = 0  # Global variable

def increment():
    global count  # Modifies the global variable
    count += 1

increment()
increment()
print(count)  # Output: 2 (but hard to track in large programs)

Good Code:

def increment(count):
    return count + 1  # Keeps the variable local

count = 0
count = increment(count)
count = increment(count)
print(count)  # Output: 2 (explicit and predictable)
# Using a class to encapsulate state
class Counter:
    def __init__(self):
        self.count = 0

    def increment(self):
        self.count += 1
        return self.count

counter = Counter()
print(counter.increment())  # 1
print(counter.increment())  # 2

Global variables are the lazy way to manage state. Keep them contained, pass values explicitly, and make debugging easier on yourself.


10. Not Using List Comprehensions

The Mistake

Writing unnecessary for loops instead of using list comprehensions makes your code slower and harder to read. Python provides a clean, efficient way to generate lists in a single line, yet many developers still clutter their code with old-school loops.

How to Avoid It

Use list comprehensions whenever you’re transforming or filtering data into a new list. They’re faster, more concise, and easier to read.

Bad Code:

numbers = [1, 2, 3, 4, 5]
squared = []
for num in numbers:
    squared.append(num ** 2)
print(squared)  # [1, 4, 9, 16, 25]

Good Code:

numbers = [1, 2, 3, 4, 5]
squared = [num ** 2 for num in numbers]
print(squared)  # [1, 4, 9, 16, 25]
# Filtering with a list comprehension
evens = [num for num in numbers if num % 2 == 0]
print(evens)  # [2, 4]

Looping manually when a list comprehension would do the job is just unnecessary. Keep your code clean and use the tools Python gives you.


11. Ignoring Python’s Built-in Functions

The Mistake

Writing custom logic when Python already has a built-in function for the job is a waste of time and often leads to slower, less readable code. Python’s standard library is packed with efficient, well-tested functions—use them.

How to Avoid It

Before writing a loop or complex logic, check if Python has a built-in function that does the same thing. Functions like sum(), max(), min(), sorted(), any(), and all() can replace unnecessary loops and conditions.

Bad Code:

numbers = [1, 2, 3, 4, 5]

# Manually summing a list
total = 0
for num in numbers:
    total += num
print(total)  # 15
# Checking if at least one item is True
flags = [False, False, True, False]
found = False
for flag in flags:
    if flag:
        found = True
        break
print(found)  # True

Good Code:

# Using built-in sum()
numbers = [1, 2, 3, 4, 5]
print(sum(numbers))  # 15
# Using any()
flags = [False, False, True, False]
print(any(flags))  # True
# Using max()
values = [10, 50, 25]
print(max(values))  # 50

If Python has a built-in function for something, use it. It’s almost always faster and cleaner than reinventing the wheel.


12. Confusing List Copying Methods

The Mistake

Assigning a list to another variable (list_b = list_a) doesn’t create a new list—it just creates a reference to the same object. Modify one, and the other changes too. This leads to unintended side effects that are hard to track down.

How to Avoid It

Use .copy(), slicing ([:]), or copy.deepcopy() (for nested structures) to create true copies instead of references.

Bad Code:

list_a = [1, 2, 3]
list_b = list_a  # This doesn't create a new list
list_b.append(4)

print(list_a)  # [1, 2, 3, 4] (unexpected change)
print(list_b)  # [1, 2, 3, 4]

Good Code:

# Shallow copy using copy()
list_a = [1, 2, 3]
list_b = list_a.copy()
list_b.append(4)

print(list_a)  # [1, 2, 3] (unchanged)
print(list_b)  # [1, 2, 3, 4]
# Using slicing
list_b = list_a[:]  
# Deep copy for nested lists
import copy

nested_list = [[1, 2], [3, 4]]
deep_copy = copy.deepcopy(nested_list)
deep_copy[0].append(99)

print(nested_list)  # [[1, 2], [3, 4]] (unchanged)
print(deep_copy)  # [[1, 2, 99], [3, 4]]

If you copy a list the wrong way, expect weird bugs. Always use .copy() or slicing, and if your list has nested structures, use copy.deepcopy().


13. Off-by-One Errors in Indexing

The Mistake

Accessing the wrong index by miscounting, forgetting that Python uses zero-based indexing, or using range(len(lst)) incorrectly leads to unexpected bugs. This is especially common when looping through lists or slicing strings.

How to Avoid It

Use enumerate() instead of manually tracking indices, and always double-check start/end values when slicing. Remember that Python indexing starts at 0, and list slicing excludes the end index.

Bad Code:

numbers = [10, 20, 30, 40, 50]

# Wrong end index
print(numbers[1:4])  # [20, 30, 40] (not including index 4)
# Manual index tracking is error-prone
for i in range(len(numbers)):
    print(i, numbers[i])  # Works but can lead to off-by-one mistakes

Good Code:

# Use enumerate to avoid index errors
for index, value in enumerate(numbers):
    print(index, value)
# Correct slicing
print(numbers[1:5])  # [20, 30, 40, 50]
# Avoid out-of-range errors
print(numbers[-1])  # 50 (last element without worrying about length)

Off-by-one errors are some of the hardest to spot. Stick to enumerate(), be mindful of zero-based indexing, and double-check slice boundaries.


14. Not Understanding Variable Scope

The Mistake

Assuming that a variable inside a function affects global variables or that modifying a global variable inside a function works without explicitly declaring it. Python has local, global, and nonlocal scopes, and if you don’t understand how they work, you’ll run into unexpected behavior.

How to Avoid It

Variables inside a function are local unless explicitly declared global or nonlocal. If you need to modify a global variable inside a function, use global (rarely a good idea). If working with nested functions, use nonlocal to modify an outer function’s variable.

Bad Code:

count = 0

def increment():
    count += 1  # UnboundLocalError: count is local but not defined
    print(count)

increment()

Good Code:

# Correct way to modify a global variable
count = 0

def increment():
    global count  # Explicitly declare global
    count += 1
    print(count)

increment()  # 1
increment()  # 2
# Using nonlocal for modifying a variable in an outer function
def outer():
    num = 10
    def inner():
        nonlocal num
        num += 5
        print(num)
    inner()
    print(num)  # 15

outer()

If you don’t understand Python’s scope rules, your variables will behave in ways you don’t expect. Be explicit about global and nonlocal, and avoid modifying global state unless absolutely necessary.


15. Using print() for Debugging Instead of Proper Tools

The Mistake

Throwing print() statements everywhere to debug might seem quick, but it’s inefficient. It clutters your code, makes debugging harder in larger projects, and doesn’t provide deeper insights like breakpoints or stack traces.

How to Avoid It

Use Python’s built-in debugging tools like pdb or logging instead of print(). This gives you more control over debugging without modifying your actual code.

Bad Code:

def add(a, b):
    print(f"Debug: a = {a}, b = {b}")  # Clutters output
    return a + b

print(add(2, 3))

Good Code:

import logging

logging.basicConfig(level=logging.DEBUG)
logging.debug("This is a debug message")

def add(a, b):
    logging.debug(f"a = {a}, b = {b}")
    return a + b

print(add(2, 3))
# Using Python’s built-in debugger
import pdb

def add(a, b):
    pdb.set_trace()  # Pause execution here
    return a + b

print(add(2, 3))  # Now you can inspect variables interactively

print() debugging is fine for quick tests, but if you’re serious about writing maintainable code, use a proper debugger or logging. Your future self will thank you.


16. Misusing if __name__ == '__main__'

The Mistake

Not using if __name__ == '__main__' when writing scripts means your code automatically runs when imported as a module, which can cause unintended side effects. This is especially bad if your script contains database operations, file modifications, or API calls.

How to Avoid It

Always wrap script execution inside if __name__ == '__main__'. This ensures that your code only runs when executed directly, not when imported.

Bad Code:

# This runs immediately even when imported
print("Running script...")

def main():
    print("Doing something important")

main()
# Importing this module in another script triggers execution
import my_script  # Unintended print statement executes

Good Code:

def main():
    print("Doing something important")

if __name__ == '__main__':
    main()  # Runs only when executed directly
# Now, importing this module won't trigger execution
import my_script  # No output unless explicitly called

If you don’t wrap your execution logic properly, your script will run at the wrong time. Use if __name__ == '__main__' to prevent unintended side effects.


17. Not Optimizing Loops and Iterations

The Mistake

Writing inefficient loops that process more data than necessary, iterate when a built-in function could do the job, or use range(len(lst)) instead of direct iteration. This leads to slower, clunkier code that wastes CPU cycles.

How to Avoid It

Use Python’s optimized built-in functions (map(), filter(), zip(), enumerate()) whenever possible. Avoid unnecessary index lookups and rewrite loops to be more Pythonic.

Bad Code:

# Unnecessary manual iteration
numbers = [1, 2, 3, 4, 5]
squared = []
for num in numbers:
    squared.append(num ** 2)
print(squared)  # [1, 4, 9, 16, 25]
# Using range(len(lst)) instead of direct iteration
for i in range(len(numbers)):
    print(numbers[i])

Good Code:

# Use list comprehensions
squared = [num ** 2 for num in numbers]
print(squared)  # [1, 4, 9, 16, 25]
# Use enumerate to avoid manual indexing
for index, value in enumerate(numbers):
    print(index, value)
# Use map() for transformations
squared = list(map(lambda x: x ** 2, numbers))
print(squared)  # [1, 4, 9, 16, 25]

Writing inefficient loops is an easy way to slow down your code. Use Python’s built-in tools and iterate smarter, not harder.


18. Hardcoding Sensitive Information

The Mistake

Storing API keys, passwords, database credentials, or other sensitive information directly in your code is a security risk. If your code is ever pushed to a public repo or shared, those credentials become exposed—making your app an easy target for attackers.

How to Avoid It

Use environment variables, configuration files, or secret management tools to store sensitive information instead of hardcoding them in your scripts.

Bad Code:

API_KEY = "my_secret_api_key"  # Hardcoded secret (bad practice)

def connect_to_service():
    return f"Connecting with API key: {API_KEY}"
DATABASE_PASSWORD = "supersecurepassword"  # Visible in plain text

Good Code:

import os

API_KEY = os.getenv("API_KEY")  # Get from environment variables

def connect_to_service():
    return f"Connecting with API key: {API_KEY}"
# Set the API key in your terminal (Mac/Linux)
export API_KEY="my_secret_api_key"

# Set the API key in Windows
set API_KEY=my_secret_api_key
# Use dotenv to manage secrets in a .env file
from dotenv import load_dotenv
import os

load_dotenv()  # Load environment variables from .env file

API_KEY = os.getenv("API_KEY")  # Now it's stored securely

Hardcoding secrets is one of the biggest security mistakes you can make. Use environment variables or secret managers—your future self (and your security team) will thank you.


19. Failing to Use Proper String Formatting

The Mistake

Using + to concatenate strings or % formatting instead of modern methods makes your code harder to read and prone to errors. String concatenation with + is inefficient, especially in loops, and older % formatting is less flexible than f-strings.

How to Avoid It

Use f-strings (f"{var}") for clean, efficient, and readable string formatting. If you’re working with multiple replacements or complex formatting, use .format().

Bad Code:

name = "Alice"
age = 30

# Inefficient and hard to read
greeting = "Hello, " + name + "! You are " + str(age) + " years old."
print(greeting)

# Outdated % formatting
greeting = "Hello, %s! You are %d years old." % (name, age)
print(greeting)

Good Code:

# Clean and efficient f-strings
name = "Alice"
age = 30
greeting = f"Hello, {name}! You are {age} years old."
print(greeting)
# Using format() for older Python versions
greeting = "Hello, {}! You are {} years old.".format(name, age)
print(greeting)
# Formatting numbers cleanly
price = 49.99
print(f"Price: ${price:.2f}")  # Price: $49.99

F-strings are the best way to format strings in Python—faster, cleaner, and more readable. If you’re still using + or %, it’s time to upgrade.


20. Not Using Generators for Large Data Processing

The Mistake

Loading massive datasets into memory all at once when you don’t need to is inefficient. If you’re processing a large file or a stream of data, using a list to hold everything can cause high memory usage and slow performance.

How to Avoid It

Use generators instead of lists when working with large data sets. Generators process items one at a time instead of storing everything in memory. This makes them ideal for iterating over large files, database records, or streamed data.

Bad Code:

# Loads all lines into memory (bad for large files)
with open("large_file.txt") as f:
    lines = f.readlines()  # Holds all lines in a list
    for line in lines:
        print(line.strip())
# Creating a list when a generator would work
numbers = [x ** 2 for x in range(10**6)]  # Consumes a lot of memory

Good Code:

# Use a generator to process file lines one by one
with open("large_file.txt") as f:
    for line in f:
        print(line.strip())  # No memory overhead
# Use a generator expression instead of a list comprehension
numbers = (x ** 2 for x in range(10**6))  # Uses almost no memory
# Define a generator function for better efficiency
def number_generator(n):
    for i in range(n):
        yield i ** 2  # Yields one result at a time

gen = number_generator(10**6)

If you’re working with large data sets and not using generators, you’re wasting memory for no reason.


21. Misunderstanding Python’s Boolean Evaluation

The Mistake

Writing unnecessary comparisons like if x == True: instead of if x: or failing to understand how Python treats values as truthy or falsy. This leads to redundant code and unexpected behavior when dealing with empty lists, dictionaries, or custom objects.

How to Avoid It

Know Python’s built-in truthy and falsy values. Use direct conditions instead of explicit comparisons to True or False.

Bad Code:

x = True
if x == True:  # Redundant comparison
    print("x is True")

if len(my_list) > 0:  # Unnecessary length check
    print("List is not empty")
# Failing to check for falsy values
value = None
if value:
    print("This runs even if value is an empty list or dict!")

Good Code:

# Direct boolean evaluation
if x:
    print("x is True")

if my_list:  # Cleaner way to check for non-empty lists
    print("List is not empty")

# Correct way to check for None
if value is not None:
    print("Value exists")
# Understanding truthy and falsy values
if not my_list:  # Correct way to check if a list is empty
    print("List is empty")

Python treats None, 0, "", [], {}, and set() as falsy—so there’s no need for explicit == False or length checks. Write conditions that align with how Python actually evaluates truthiness.


22. Overlooking Dictionary Methods

The Mistake

Manually checking for dictionary keys or updating values inefficiently instead of using Python’s built-in dictionary methods. This leads to unnecessary lines of code and potential key errors.

How to Avoid It

Use dict.get() to retrieve values safely, setdefault() to initialize keys, and dictionary unpacking for clean updates.

Bad Code:

# Checking for a key before accessing it
if "name" in user_data:
    name = user_data["name"]
else:
    name = "Guest"

# Manually updating a dictionary
if "count" in stats:
    stats["count"] += 1
else:
    stats["count"] = 1

Good Code:

# Use .get() to avoid KeyError
name = user_data.get("name", "Guest")  # Default to "Guest" if missing

# Use setdefault() to initialize a key if it doesn’t exist
stats.setdefault("count", 0)
stats["count"] += 1

# Dictionary merging (Python 3.9+)
defaults = {"theme": "light", "notifications": True}
user_settings = {"theme": "dark"}

settings = {**defaults, **user_settings}  # Merges dictionaries
print(settings)  # {'theme': 'dark', 'notifications': True}

Manually checking keys before accessing them is unnecessary in most cases. Use dictionary methods—they exist for a reason.


23. Mismanaging Threading and Multiprocessing

The Mistake

Using threads for CPU-intensive tasks or spawning unnecessary processes leads to performance bottlenecks and resource exhaustion. Python’s Global Interpreter Lock (GIL) prevents threads from running CPU-bound tasks in parallel, so using threading for heavy computations is useless.

How to Avoid It

Use multiprocessing for CPU-bound tasks and threading for I/O-bound tasks (like network requests or file operations). Understand the difference before choosing one.

Bad Code:

import threading

def compute():
    total = sum(x**2 for x in range(10**6))

# This won’t improve performance due to the GIL
threads = [threading.Thread(target=compute) for _ in range(4)]
for thread in threads:
    thread.start()
for thread in threads:
    thread.join()

Good Code:

import multiprocessing

def compute():
    return sum(x**2 for x in range(10**6))

if __name__ == "__main__":
    with multiprocessing.Pool(processes=4) as pool:
        results = pool.map(compute, range(4))
import threading
import requests

def fetch_url(url):
    response = requests.get(url)
    print(f"Fetched {url}: {response.status_code}")

# Use threads for network-bound tasks
urls = ["https://example.com"] * 5
threads = [threading.Thread(target=fetch_url, args=(url,)) for url in urls]

for thread in threads:
    thread.start()
for thread in threads:
    thread.join()

If you don’t know whether to use threading or multiprocessing, stop and think. Threads don’t speed up CPU work, and processes aren’t worth it for I/O. Pick the right tool for the job.


24. Importing Unnecessary Modules

The Mistake

Importing entire libraries when only a few functions are needed clutters your code and increases memory usage. Worse, wildcard imports (from module import *) can overwrite existing variables, making debugging harder.

How to Avoid It

Only import what you need. If you’re using a small part of a module, import just that function or class instead of the whole thing. Avoid wildcard imports unless absolutely necessary.

Bad Code:

import math  # Unnecessary if you're only using one function

print(math.sqrt(25))
from random import *  # Pollutes the namespace
print(randint(1, 10))  # Where does this function come from?

Good Code:

from math import sqrt  # Import only what's needed
print(sqrt(25))
# Use aliases for clarity if needed
import numpy as np
array = np.array([1, 2, 3])
# Avoid wildcard imports
from random import randint  # Now it's clear where randint comes from
print(randint(1, 10))

Unnecessary imports slow down execution and make your code harder to maintain. Keep imports clean and explicit—you’ll thank yourself later.


25. Not Keeping Up with Python Updates

The Mistake

Sticking to outdated Python versions or ignoring new language features means missing out on performance improvements, security fixes, and cleaner syntax. If you’re still writing Python like it’s 2010, you’re making life harder for yourself.

How to Avoid It

Stay updated with the latest stable Python release and take advantage of new features. Use pyenv or Docker to manage different Python versions for compatibility testing.

Bad Code (Old Python Practices):

# Old-style string formatting (Deprecated in favor of f-strings)
name = "Alice"
greeting = "Hello, %s!" % name

# Using range(len()) instead of enumerate()
numbers = [10, 20, 30]
for i in range(len(numbers)):
    print(i, numbers[i])

Good Code (Modern Python):

# f-strings (Python 3.6+)
name = "Alice"
greeting = f"Hello, {name}!"

# Use enumerate() instead of manual indexing
numbers = [10, 20, 30]
for index, value in enumerate(numbers):
    print(index, value)
# Dictionary merging (Python 3.9+)
defaults = {"theme": "light", "notifications": True}
user_settings = {"theme": "dark"}
settings = defaults | user_settings  # Merges dictionaries
# Pattern matching (Python 3.10+)
def process(value):
    match value:
        case 1:
            print("One")
        case 2:
            print("Two")
        case _:
            print("Something else")

If you’re not using modern Python, you’re working harder than you need to. Keep your Python version updated and take advantage of the improvements—it’s free performance and cleaner code.


No More Rookie Mistakes

Most Python mistakes aren’t about syntax—they’re about bad habits. Whether it’s inefficient loops, misusing built-in functions, or ignoring best practices, these small errors add up. The good news? Now you know better.

Clean, optimized, and idiomatic Python isn’t just about writing better code—it makes you a better developer. Start catching these mistakes in your own projects, and you’ll write faster, cleaner, and more maintainable code that doesn’t break under pressure.

Want to rep your Python skills in style? Check out the merch.


Leave a Reply

Your email address will not be published. Required fields are marked *