I Reviewed 500+ Pull Requests — Here’s What Every Python Dev Gets Wrong
After reviewing hundreds of PRs, patterns start to emerge. And some of them are surprisingly consistent — even among experienced Python…

They Thought They Nailed It — They Didn’t
I Reviewed 500+ Pull Requests — Here’s What Every Python Dev Gets Wrong
After reviewing hundreds of PRs, patterns start to emerge. And some of them are surprisingly consistent — even among experienced Python developers. Here’s what I’ve seen, and what you can do better.
The Invisible Code Smells You Might Be Shipping
I’ve been reviewing pull requests almost daily for the past few years — across startups, open-source projects, and client work. Python is often the common denominator. It’s a beautiful language, sure, but it also makes it dangerously easy to write poor-quality code that looks fine at first glance.
After combing through 500+ PRs, I noticed something alarming: even skilled Python developers repeatedly make the same subtle (but critical) mistakes. These aren’t just beginner oversights — they’re bad habits, misused language features, and readability killers that sneak past linters and CI pipelines.
So if you’re pushing Python code, this one’s for you.
1. Misusing Default Mutable Arguments
This one’s a classic, and it’s still everywhere.
def append_item(item, my_list=[]):
my_list.append(item)
return my_list
What’s wrong? That []
is a trap. It gets evaluated once at function definition—not every time the function is called. So the list keeps growing across calls, unintentionally.
The fix:
def append_item(item, my_list=None):
if my_list is None:
my_list = []
my_list.append(item)
return my_list
If you’re still doing the first one, it’s time to stop.
It’s not just a bug — it’s a ticking time bomb in your API.
2. Writing Code That’s “Too Pythonic”
There’s a fine line between writing clean idiomatic code and overdoing the Pythonic one-liners.
Take this:
result = [func(x) for x in items if condition(x) and other_check(x) and yet_another(x)]
Or worse:
value = next((x for x in items if x > 10 and x % 2 == 0), None)
Sure, it’s compact. But is it readable? Maintainable? Debbugable? That’s where most PRs fail — not in correctness, but in empathy for the next developer.
Remember: Just because you can doesn’t mean you should.
3. Reinventing the Standard Library
Python’s standard library is a treasure trove. But I still see PRs with code like this:
def flatten(list_of_lists):
return [item for sublist in list_of_lists for item in sublist]
Looks clever. But why not use:
import itertools
flattened = list(itertools.chain.from_iterable(list_of_lists))
Or re-implementing Counter
, defaultdict
, namedtuple
, Pathlib
logic—you name it.
If you’re writing more than 5 lines to do something common, check the standard library first.
4. Abusing try
-except
as Flow Control
I get it — Python makes exceptions easy. But I’ve seen this far too often:
try:
value = my_dict['key']
except KeyError:
value = 'default'
Or even:
try:
risky_call()
except:
pass # 😱
This is not just lazy — it’s dangerous.
Better:
value = my_dict.get('key', 'default')
Or for errors:
try:
risky_call()
except SpecificError as e:
logger.error("Handled error: %s", e)
Catching everything is not resilience — it’s recklessness.
5. Not Writing Enough Tests (Or the Right Ones)
You’d be shocked how many PRs introduce new logic without a single test. Even worse? Tests that only cover the happy path.
Good PRs include:
- Unit tests for all new functions/classes
- Edge case coverage
- Clear setup and teardown logic
- No dependency on external state
If your test passes but breaks when someone changes one line of unrelated code, it’s not a real test.
6. Poor Naming and Inconsistent Style
This one is subtle but brutal for team velocity.
Here’s what I often see:
- Variables named
data
,info
, ortemp
- Mixed camelCase and snake_case in the same file
- Function names like
do_process()
orhandle_stuff()
Code is read more than it’s written. Every bad name is a tax on your teammates.
Fix it by:
- Following PEP8 religiously
- Naming things based on intent, not implementation
- Using linters and formatters (
black
,flake8
,isort
) as pre-commit hooks
You don’t get points for clever naming. You get points for clarity.
7. Ignoring Async — Even When It’s Needed
Many developers still write blocking I/O code in async-capable services like FastAPI or asyncio projects:
def fetch_data():
response = requests.get("https://api.example.com")
return response.json()
This blocks the event loop! Instead:
import httpx
async def fetch_data():
async with httpx.AsyncClient() as client:
response = await client.get("https://api.example.com")
return response.json()
If you’re working in an async context — use async libraries. Mixing sync and async is a silent performance killer.
8. Over-Engineering for “Future-Proofing”
I’ve reviewed PRs with:
- Abstract base classes no one needs
- Config layers with no configs
- Factories and services for a 50-line script
Ask yourself:
- Is this solving a real problem now?
- Will this code still make sense to a new hire in 3 months?
Write for today. Refactor when tomorrow actually comes.
So, What Makes a Great Python PR?
Here’s a mini checklist you can use before clicking “Create Pull Request”:
- Does the code follow PEP8 and use linters/formatters?
- Are function names and variables self-explanatory?
- Are edge cases handled clearly — not just caught silently?
- Is the solution readable and boring in the best way?
- Are there sufficient (and meaningful) tests?
- Could this logic be simplified using a built-in or standard library?
If you answer “no” to any of the above — revisit your PR. Future-you will thank you.
Final Thoughts
Python’s simplicity is both its greatest strength and most dangerous weakness. It’s easy to write something that works — but writing code that’s readable, maintainable, and truly “Pythonic” is a skill honed with care.
After reviewing 500+ PRs, I’ve learned this:
Great Python code isn’t clever. It’s clear.
So the next time you submit a pull request, don’t just ask:
Does it run?
Ask: Will anyone want to read this tomorrow?
