Idioms the AI writes constantly
six constructs after which Python reads freely
What it is
Python has a set of turns of phrase that an AI uses in almost every file. They are shorter than plain loops and considered "pythonic" — so the model reaches for them even when something simpler would do.
They have to be learned once. After that, generated Python reads with almost no stumbling.
What it looks like in code
from dataclasses import dataclass
from pathlib import Path
@dataclass
class City:
name: str
temp: float | None = None
cities = [City("Oslo", 4.0), City("Tromsø"), City("Bergen", 7.5)]
warm = [c.name for c in cities if c.temp is not None and c.temp > 5]
print("warm:", warm)
for i, c in enumerate(cities, start=1):
print(f"{i}. {c.name}: {c.temp if c.temp is not None else 'no data'}")
codes = ["OSL", "TOS", "BGO"]
print(dict(zip(codes, (c.name for c in cities))))
p = Path("/tmp/pyidiom/idioms.py")
print(p.name, "|", p.suffix, "|", p.exists())Output:
warm: ['Bergen']
1. Oslo: 4.0
2. Tromsø: no data
3. Bergen: 7.5
{'OSL': 'Oslo', 'TOS': 'Tromsø', 'BGO': 'Bergen'}
idioms.py | .py | True1. The list comprehension
warm = [c.name for c in cities if c.temp is not None and c.temp > 5]It reads left to right like a sentence: collect c.name for every c in
cities where the temperature is above five.
There are three forms, differing only in the brackets:
| Written | What you get |
|---|---|
[x for x in ...] | a list |
{k: v for ...} | a dictionary |
(x for x in ...) | a generator — computed as you walk it |
The third form saves memory on large data, but it cannot be walked twice: after the first pass the generator is empty.
2. @dataclass
@dataclass
class City:
name: str
temp: float | None = NoneIt spares you the __init__ and gives a readable printout. When the AI
proposes a class purely to hold data, it should be a @dataclass (article
2.7).
3. enumerate
for i, c in enumerate(cities, start=1):The index and the item at once. If you see for i in range(len(x)), that is
the same loop written awkwardly.
4. zip
dict(zip(codes, names))Walks two lists in parallel. An important detail: zip quietly stops at the
shorter one. If the lists differ in length, some of the data disappears
without a single error.
5. pathlib
p = Path("/tmp/pyidiom/idioms.py")
p.name, p.suffix, p.exists()
p.parent / "other.py"The modern replacement for os.path. The slash instead of os.path.join is
not a typo but an overloaded operator.
6. f-strings
f"{c.name}: {c.temp if c.temp is not None else 'no data'}"Inside the braces is an ordinary expression. Useful additions:
| Written | What it does |
|---|---|
f"{x!r}" | shows the quotes and the invisible (article 4.5) |
f"{x:.2f}" | two decimal places |
f"{x=}" | prints x=value — handy while debugging |
What to tell the AI
Rewrite this list comprehension as a plain loop.
Don't change the behaviour.This class only holds data. Turn it into a dataclass.The reverse technique — limiting the pythonic ingenuity up front:
Keep it simple: plain loops rather than nested comprehensions,
no walrus operator, no one-liners. People other than you and
me will have to read this.What to look for
if x: where zero is a normal value. if c.temp: throws away both
None and 0.0: a frost of zero degrees becomes "no data". An empty
string, 0 and [] are falsy too. When you are checking "is there a
value", write is not None. An AI mixes these two checks up constantly.
Nested comprehensions. [y for x in data for y in x if y] is formally
pythonic and practically unreadable. Two levels is the limit.
zip on lists of different lengths. It silently truncates to the
shorter one.
A generator where a second pass is needed. The second walk gives you nothing, with no error.
The walrus operator :=. if (n := len(data)) > 10: saves a line and
complicates reading. The AI uses it more often than it should.
A dict instead of a dataclass. data["temp"] breaks on a typo in
the key, city.temp does not. If the structure is known in advance, it
belongs in a class.
Cheat sheet
Comprehensions:
[...]a list,{k: v ...}a dictionary,(...)a generator.A generator can be walked once.
@dataclass— a class for data only.enumerateinstead ofrange(len(...)),zipfor two lists.zipsilently truncates to the shorter list.f"{x!r}"andf"{x=}"for debugging;pathlibinstead ofos.path.