AlgoMaster Logo

Pythonic Code (Idioms & Anti-patterns)

High Priority29 min readUpdated September 21, 2026
Listen to this chapter
Unlock Audio

"Pythonic" code is code that reads the way Python was designed to be read: short, direct, and built out of the language's own idioms instead of patterns borrowed from other languages. This lesson covers what that actually means in practice, the Zen of Python, the EAFP style, idiomatic iteration and unpacking, the standard-library helpers that replace hand-rolled checks, and the anti-patterns that keep showing up in code reviews.

What "Pythonic" Means

A snippet is Pythonic when a fluent Python reader can scan it once and understand intent without translating idioms in their head. The opposite is code that works but reads like it was ported line-by-line from another language: manual index variables, len(x) == 0 checks, dict.has_key-style guards, type(x) == list comparisons. The code runs, but every line costs the reader a small tax.

Python ships a half-joking style guide that captures the philosophy. Open a REPL and type:

The twenty aphorisms in the Zen aren't rules you can mechanically apply. They're tiebreakers. When two implementations both work, the Zen tells you which one to prefer: the readable one, the explicit one, the one with fewer special cases. Most "Pythonic vs non-Pythonic" arguments come down to which line in the Zen you're optimizing for.

A small example to make this concrete. Two functions that count in-stock products:

Both return the same number. The second one tells the reader "I'm counting products that satisfy a condition" in a single line. The first one makes the reader run a small simulation in their head.

EAFP vs LBYL

There are two general styles for handling the case where an operation might not work: check first (LBYL, "Look Before You Leap") or just try it and catch the failure (EAFP, "Easier to Ask Forgiveness than Permission"). Python's culture leans heavily toward EAFP, and the language is built to support it.

LBYL: check that the key exists before reading it.

EAFP: try the read, handle the miss.

For a single dict lookup, neither version is dramatic, and most Python code just uses cart.get("bread", 0). The difference matters more when the check and the action can race or when "looking" is expensive.

The diagram makes the structural difference visible. LBYL branches on a predicate before doing the work. EAFP does the work and only branches if it fails. EAFP wins in three situations: when the check is racy (the value might change between the check and the use), when the check duplicates work the operation will do anyway (re-walking a dict, re-parsing a string), and when the failure case is rare so the exception cost doesn't matter.

EAFP isn't a blanket rule. If the predicate is cheap and the operation has side effects you can't undo, check first. The point is that Python treats exceptions as a normal control-flow tool, not a panic button.

Truthiness and Idiomatic Boolean Checks

Every Python object has a truth value. The language defines a small set of "falsy" values, and everything else is truthy. The falsy set: False, None, 0, 0.0, 0j, empty containers ("", [], (), {}, set()), and any object whose __bool__ returns False (or whose __len__ returns 0).

That lets you write conditions against the value itself instead of testing properties of it.

The Pythonic version is shorter and matches how you'd say it out loud: "if there's no cart", "if there's a customer name." It also picks up edge cases for free. if customer_name: is false for None, the empty string, and any other empty-ish input, which is usually what you want.

There's one trap. if value: and if value is not None: are not the same. If value could legitimately be 0, 0.0, "", or [] and you only want to skip None, use the explicit comparison.

PEP 8 also says: compare to None, True, and False with is, not ==. There's only one None in any running Python program, so identity is the correct check.

Unpacking

Unpacking is one of the features that makes Python feel different from other languages. Instead of indexing into a sequence, you let Python assign the pieces to named variables in a single expression.

Basic tuple unpacking:

Swapping values without a temp variable:

Starred assignment captures "the rest" into a list. You can put the star at the front, middle, or end:

Functions accept and forward arguments using *args (positional) and **kwargs (keyword). The most common use is forwarding arguments to another function without listing every parameter:

Dictionaries unpack with ** into another dict or into keyword arguments:

Iterating Pythonically

Loops are where non-Pythonic habits show up most often. Python wants you to iterate over the thing directly, not over its indices.

The four iteration helpers that replace most manual index tracking:

HelperUse when
enumerate(seq)You need both the index and the item
zip(a, b)You're walking two or more sequences in lockstep
dict.items()You need both keys and values from a dict
reversed(seq)You want to iterate from the end back to the start

Side-by-side examples:

enumerate also takes a start argument when you want one-based numbering:

There's also a question of when to build a list versus when to stream. If you're consuming the result once and never again, a generator expression keeps you from allocating a full list in memory:

For five items it doesn't matter. For a million-row CSV, the second version reads one row at a time and never holds the full list.

Comprehensions vs map and filter

For most transformations, Python prefers a comprehension over map or filter. The short version is that comprehensions read left-to-right in the same way you'd describe the operation in English.

map and filter still have niches. If you're passing a named function (not a lambda) and don't need a list at the end, map(func, iterable) is fine and lazy. But the moment you reach for lambda, a comprehension is usually clearer.

Context Managers for Resources

Anything that has to be released after use, files, locks, network connections, database cursors, belongs inside a with block. The block guarantees the cleanup runs even if the body raises.

If handle.read() raises in the first version, handle.close() never runs and the file descriptor leaks. The with version closes the file whether the body succeeds or raises.

You can open multiple resources in one with:

The takeaway here: if a function returns something with a close() method, your first instinct should be with.

Standard-Library Helpers Over Manual Checks

Half of the non-Pythonic dict code in the wild reinvents something the standard library already does in one line.

dict.get(key, default) returns the default when the key is missing:

dict.setdefault(key, default) is get plus "insert the default if missing." Useful when you're grouping items:

collections.defaultdict is even cleaner when every miss should produce the same default:

collections.Counter replaces hand-rolled frequency tables:

Counter also supports addition, subtraction, and intersection, which is handy for inventory math:

The point here is recognizing the pattern: any time you're writing if key in d followed by initialization, one of these helpers probably already does it.

pathlib Over os.path Strings

The os.path module treats paths as strings, which means every operation is a function call that takes strings and returns strings. pathlib, added in Python 3.4, gives you a Path object with methods, and most operations chain.

Path overloads / for joining, so you write paths the way they look on disk. The object has methods for reading (read_text, read_bytes), writing (write_text, write_bytes), checking (exists, is_file, is_dir), listing (iterdir, glob), and inspecting (name, stem, suffix, parent).

A common task, listing all CSVs in a folder, becomes one line:

For new code, pathlib is the default. Keep os.path around only for two cases: working with code that already uses strings everywhere, or interacting with libraries whose APIs only accept strings (most modern libraries accept either).

f-strings for Formatting

Python has had three string-formatting styles. f-strings (Python 3.6+) replace both of the older ones for almost every use.

f-strings let you embed any expression, not just names:

Python 3.8 added = for quick debug output. f"{name=}" expands to name='Priya', which saves writing the label twice when you're debugging:

Use the older styles only when you have to: %-formatting is still common in logging (logging.info("user %s logged in", user) defers the formatting until the record is actually emitted), and str.format is occasionally useful for templates loaded from a file. For everything else, reach for f-strings first.

Common Anti-patterns

A handful of patterns keep showing up in code review even on otherwise solid Python.

Comparing types with `==`. Use isinstance instead. It handles subclasses correctly and is the convention.

If you legitimately need an exact type and not a subclass, the explicit type(value) is list is at least clearer than ==, but cases where you need that are rare.

Manual indexing in loops. Already covered above. Use enumerate, zip, or dict.items() instead of range(len(...)).

String concatenation in a loop. Each += on a string allocates a new string, so a loop that concatenates n strings does O(n^2) work in the worst case. Use str.join instead.

Mutating a list while iterating over it. The iterator's internal index doesn't notice your deletions, so items get skipped or revisited.

If you need to mutate the original list object (other code holds a reference), use slice assignment:

Bare `except:` clauses. A bare except catches every exception, including KeyboardInterrupt and SystemExit, which you almost never want to swallow.

If you really do want to catch every "normal" exception (in a logging or supervisor layer), use except Exception:. That still lets KeyboardInterrupt propagate.

**from module import *.** Star imports drag everything from a module into your namespace, including things you didn't know existed. They shadow your own names without warning and break tools that try to figure out what's defined where.

Star imports are tolerable in two places: a package's __init__.py that re-exports a curated __all__, and an interactive REPL where you don't care. In application code, they're noise.

The decision tree above is the quick lookup for the iteration anti-patterns. Most code reviews where someone wrote for i in range(len(x)): end with one of those four answers.

Quiz

Pythonic Code Quiz

10 quizzes