Programming with AI
ENRU
6.2

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:

measured
warm: ['Bergen']
1. Oslo: 4.0
2. Tromsø: no data
3. Bergen: 7.5
{'OSL': 'Oslo', 'TOS': 'Tromsø', 'BGO': 'Bergen'}
idioms.py | .py | True

1. 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:

WrittenWhat 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 = None

It 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:

WrittenWhat 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

Prompt
Rewrite this list comprehension as a plain loop.
Don't change the behaviour.
Prompt
This class only holds data. Turn it into a dataclass.

The reverse technique — limiting the pythonic ingenuity up front:

Prompt
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.

  • enumerate instead of range(len(...)), zip for two lists.

  • zip silently truncates to the shorter list.

  • f"{x!r}" and f"{x=}" for debugging; pathlib instead of os.path.