Most "unpythonic" code isn't wrong — it works. It's just unnecessary indirection, defensive coding that doesn't earn its weight, or imports of styles from other languages. Recognizing these patterns and pruning them makes code shorter, faster to read, and easier to maintain.
1. range(len(seq)) — when you really want enumerate
If you're iterating for i in range(len(seq)) just to access seq[i], you almost certainly want for i, x in enumerate(seq). The exception: when you genuinely don't need the value (just the index, for some computation). Even then, ask yourself why.
2. Using == to compare with None
x == None works but isn't the idiom. Use x is None / x is not None. None is a singleton; identity comparison is faster and reads better. Same applies to True and False in the rare cases you'd compare against them at all.
3. Verbose dict iteration
for key in d.keys(): when you're not using key.foo() on the key view: just for key in d:. for key in d.keys(): print(d[key]): just for value in d.values():. for key in d.keys(): print(key, d[key]): just for key, value in d.items():.
4. Default mutable arguments
def f(x=[]): covered repeatedly. The shared-default-list bug. Use def f(x=None): if x is None: x = []. Memorize it; the bug is subtle and easy to introduce.
5. Catching too broadly
except: catches BaseException including KeyboardInterrupt. except Exception: is broad but acceptable. Better: catch the specific exception types you can recover from. Other exceptions should propagate.
6. Optimizing without measuring
Premature optimization is the bug-introducing kind. Profile (tooling track) first; optimize where the time actually is. Don't skip arrays, write C extensions, or vectorize before measuring — you'll spend hours on code that wasn't slow.
Code
range(len()) — usually wrong·python
items = ["a", "b", "c"]
# UNPYTHONIC
for i in range(len(items)):
print(i, items[i])
# PYTHONIC
for i, x in enumerate(items):
print(i, x)
# Pair iteration with zip
names = ["alice", "bob"]
ages = [30, 25]
# UNPYTHONIC
for i in range(len(names)):
print(names[i], ages[i])
# PYTHONIC
for name, age in zip(names, ages):
print(name, age)
is None vs == None·python
x = None
# Less idiomatic
if x == None:
print("missing")
# Idiomatic
if x is None:
print("missing")
if x is not None:
print("present")
# Same for True/False on the rare occasion
# (and in those cases, just `if x:` or `if not x:` is usually better)
Dict iteration — pick the right view·python
d = {"a": 1, "b": 2, "c": 3}
# UNPYTHONIC
for key in d.keys():
print(key, d[key])
# PYTHONIC
for key, value in d.items():
print(key, value)
# Just the values
for value in d.values():
print(value)
# Just the keys
for key in d: # iterating dict gives keys
print(key)
Mutable default — fix the obvious bug·python
# DON'T — shared default list
def append_to(item, target=[]):
target.append(item)
return target
append_to("a") # ['a']
append_to("b") # ['a', 'b'] <- the bug
# DO — None sentinel pattern
def append_to(item, target=None):
if target is None:
target = []
target.append(item)
return target
append_to("a") # ['a']
append_to("b") # ['b'] <- fresh
Catching too broadly·python
# DON'T — bare except
try:
risky()
except:
pass
# Catches KeyboardInterrupt, SystemExit, MemoryError, etc.
# Acceptable — Exception is the broadest reasonable
try:
risky()
except Exception as e:
logger.error("unexpected: %s", e)
# Best — be specific
try:
risky()
except (ValueError, KeyError) as e:
handle_specifically(e)
# Other exceptions propagate — usually correct
Find three pieces of code in your own work or in any open-source Python project that exhibit one of these anti-patterns: range(len()), bare except, mutable default, or ==None. For each, write the corrected version. If you can't find any in your own code, write three deliberately bad examples and rewrite them. The point is recognition: you can't fix the patterns you don't see.
Progress
Progress is local-only — sign in to sync across devices.