8 Python Code Smells I Learned to Eliminate After Reviewing 100+ PRs
These 8 patterns don’t cause bugs immediately — but they silently make your code harder to scale, read, and maintain.

After reviewing hundreds of PRs, I stopped looking for broken code — and started spotting broken design.
8 Python Code Smells I Learned to Eliminate After Reviewing 100+ PRs
After reviewing over 100 pull requests on Python projects — from hobby apps to production-grade systems — I started to notice a pattern.
Certain issues kept popping up. Not bugs exactly, but “code smells” — those subtle signs that something isn’t quite right. Left unchecked, they snowball into tech debt, make your codebase harder to understand, and slow down everyone who touches it.
So here are the 8 most common Python code smells I learned to spot — and how to eliminate them before they grow into problems.
1. Long Functions That Do Everything
If your function scrolls past the bottom of your screen, it’s trying to do too much.
A 100-line function might work, but it hides intent. It’s harder to test, harder to debug, and harder to reuse.
Fix it:
Break the function into smaller, well-named helpers. Each should do one thing clearly. Your future self (and your teammates) will thank you.
# Instead of this:
def process_data(file_path):
# Loads, cleans, filters, writes data
pass
# Try:
def process_data(file_path):
data = load_data(file_path)
cleaned = clean_data(data)
filtered = filter_data(cleaned)
write_results(filtered)
2. Too Many Comments Explaining the Obvious
If your code needs a comment to explain what it does, maybe it’s not written clearly enough.
Comments rot. They go out of date. The best code explains itself with clear names and simple logic.
Fix it:
Refactor messy logic instead of patching it with comments. Use expressive variable and function names.
# Instead of:
# Check if user is logged in
if session.get("user_id"):
...
# Try:
if user_is_logged_in(session):
...
3. Copy-Pasted Logic Across Functions or Files
“Didn’t I just read this exact same block of code…?”
Duplicated logic becomes a nightmare to maintain. If you find yourself copy-pasting code with minor changes — stop.
Fix it:
Abstract repeated logic into reusable functions or classes.
# If you have this in multiple places:
for user in users:
if user.is_active:
send_email(user.email)
# Abstract it:
def notify_active_users(users):
for user in users:
if user.is_active:
send_email(user.email)
4. Primitive Obsession
Using strings, ints, and lists where a custom class would make more sense.
You see this a lot with things like user profiles or configurations, where a dictionary is passed around instead of a meaningful object.
Fix it:
Wrap primitives in meaningful data structures.
# Instead of:
def send_invoice(invoice_data):
total = invoice_data["amount"] + invoice_data["tax"]
# Try:
@dataclass
class Invoice:
amount: float
tax: float
def total(self):
return self.amount + self.tax
def send_invoice(invoice: Invoice):
print(invoice.total())
5. Deep Nesting and Indentation Hell
Four levels ofif
andfor
blocks = mental gymnastics.
Nested logic increases cognitive load.
Fix it:
Use early returns, guard clauses, or break up logic into smaller functions.
# Instead of:
if user:
if user.is_active:
if not user.is_banned:
do_something()
# Try:
if not user or not user.is_active or user.is_banned:
return
do_something()
6. Inconsistent Naming Conventions
user_id
,UserId
,userid
, andid_of_user
all refer to the same thing...but do they?
Inconsistent naming makes it harder to trace logic across files.
Fix it:
Stick to a naming convention (snake_case
in Python) and use consistent terminology.
Good: user_id
, get_user
, user_profile
Bad: uid
, UserDetails
, idOfUser
7. Catching Broad Exceptions
Catching all exceptions is like duct-taping a leak instead of fixing the pipe.
This hides bugs and makes debugging a nightmare.
Fix it:
Catch specific exceptions and handle them intentionally.
# Instead of:
try:
risky_operation()
except Exception:
print("Something went wrong")
# Try:
try:
risky_operation()
except FileNotFoundError:
print("File missing. Please check the path.")
8. Magic Numbers and Strings
Where did that7
come from? Why is it"gold"
? What does"abc123"
mean?
These “magic values” create hidden rules in your code.
Fix it:
Give them names. Extract them into constants or enums.
# Instead of:
if user.level > 7:
grant_gold_badge()
# Try:
GOLD_BADGE_THRESHOLD = 7
if user.level > GOLD_BADGE_THRESHOLD:
grant_gold_badge()
Final Thoughts
Reviewing code isn’t just about catching bugs — it’s about elevating quality, maintainability, and clarity. These code smells often slip through in the rush of deadlines or MVPs, but they compound over time. By learning to spot and refactor them early, you keep your codebase healthy and your team sane.
If you’ve noticed other Python code smells in your reviews, I’d love to hear about them in the comments. Let’s make Python cleaner — one PR at a time.
Enjoyed this post? Follow me for more dev insights, code reviews, and engineering culture deep dives.