Your cart is currently empty!
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.
- 1. Syntax & Indentation Errors
- 2. Misusing Mutable Default Arguments
- 3. Forgetting to Close Files
- 4. Using “is” Instead of “==” for Comparison
- 5. Modifying a List While Iterating
- 6. Ignoring Exception Handling
- 7. Catching Too Broad Exceptions
- 8. Forgetting to Use Virtual Environments
- 9. Overusing Global Variables
- 10. Not Using List Comprehensions
- 11. Ignoring Python’s Built-in Functions
- 12. Confusing List Copying Methods
- 13. Off-by-One Errors in Indexing
- 14. Not Understanding Variable Scope
- 15. Using print() for Debugging Instead of Proper Tools
- 16. Misusing if __name__ == '__main__'
- 17. Not Optimizing Loops and Iterations
- 18. Hardcoding Sensitive Information
- 19. Failing to Use Proper String Formatting
- 20. Not Using Generators for Large Data Processing
- 21. Misunderstanding Python’s Boolean Evaluation
- 22. Overlooking Dictionary Methods
- 23. Mismanaging Threading and Multiprocessing
- 24. Importing Unnecessary Modules
- 25. Not Keeping Up with Python Updates
- No More Rookie Mistakes

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
orpylint
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 yourif
,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.
Staff picks
-
$21.00
Zebra Sunglasses Tee
-
$15.00 – $19.00
Python Coding Mug – Pip Install Coffee Developer Gift
-
$21.00
Colorful Ape Sunglasses Tee
-
$21.00
Frog Sunglasses Tee
-
$21.00
Tech Cat Unisex Tee
-
$15.00 – $19.00
Python OOP Coffee Mug – Funny Developer Gift
How to Resolve ImportErrors and ModuleNotFoundErrors in Python Projects
Struggling with Python import errors? Learn how to fix ImportError and ModuleNotFoundError…
Stop Writing Python Like JavaScript – Common Mistakes and How to Fix Them
Python and JavaScript are not the same, and yet, I keep seeing…
The 2025 Developer Report: 150+ Data Points on Salaries, Skills & Trends
With AI-assisted coding, rising salaries, and shifting work environments, 2025 is shaping…
Top 25 Most Common Python Mistakes (And How to Avoid Them)
Even experienced Python developers fall into common traps—slow loops, bad exception handling,…
Stop Writing Clean Code, Write Maintainable Code Instead
Perfect Code Is a Myth. Write Code That Survives Every software engineer gets…
Why Every Python Developer Should Master List Comprehensions
List comprehensions are one of Python’s most powerful features, making code cleaner,…
Leave a Reply