New: Try Voli The Bear, Fast package manager (and not only) for Windows
Updated Jun 19, 2026 Edit on GitHub

Idioms & Common Gotchas

There's a point where Python stops feeling like "a language I'm translating my old habits into" and starts feeling like its own thing - when you learn the idioms, patterns Python programmers reach for so often that code not using them looks faintly foreign.

This phase has two halves: the idioms worth adopting on purpose, then a cheat-card of gotchas - small, surprising behaviors that bite nearly every Python programmer exactly once. Read them before they bite you; that's the point of having them in one place.

The idioms worth adopting

Comprehensions - build a list (or dict) in one expression

What it actually is. A comprehension builds a new collection by describing it, instead of starting empty and appending in a loop - close to how you'd say it out loud.

nums = [1, 2, 3, 4, 5]

# the long way
squares = []
for n in nums:
    squares.append(n * n)

# the idiom
squares = [n * n for n in nums]

# with a filter
evens = [n for n in nums if n % 2 == 0]

# a dict comprehension
lengths = {word: len(word) for word in ["hi", "hello"]}
print(squares, evens, lengths)
$ python comp.py
[1, 4, 9, 16, 25] [2, 4] {'hi': 2, 'hello': 5}

What just happened: [n * n for n in nums] produced the same list as the four-line loop, in one line reading "n squared, for each n in nums." if n % 2 == 0 filters as it builds; the dict version uses {key: value for ...}. They're not just shorter - they signal "I'm transforming a collection," easier to read than a loop whose purpose you must infer.

⚠️ Gotcha - don't cram everything in. A comprehension with two filters and a nested loop is worse than a plain loop. Use them for simple transform-and-filter; reach for a loop as logic grows.

Unpacking - pull a sequence apart into names

point = (3, 4)
x, y = point                       # unpack a tuple into two names
first, *rest = [1, 2, 3, 4]        # * grabs "everything else"
print(x, y)
print(first, rest)
$ python unpack.py
3 4
1 [2, 3, 4]

What just happened: x, y = point assigned 3 and 4 in one move; *rest swept the rest into a list. This is also how Python returns "multiple values" - a function returns a tuple and the caller unpacks it.

enumerate and zip - loop like a Python programmer

What they are. enumerate gives you the index and item while looping; zip walks two (or more) sequences in lockstep. Both replace clunky index bookkeeping.

names = ["Ana", "Bo", "Cy"]
scores = [90, 85, 95]

for i, name in enumerate(names):           # index + value, no counter needed
    print(i, name)

for name, score in zip(names, scores):     # two lists, paired up
    print(f"{name}: {score}")
$ python loops.py
0 Ana
1 Bo
2 Cy
Ana: 90
Bo: 85
Cy: 95

What just happened: enumerate(names) yielded (0, "Ana"), (1, "Bo"), ... - no manual i = 0; i += 1 counter needed. zip(names, scores) yielded ("Ana", 90), ("Bo", 85), ..., pairing the lists position by position. ⚠️ Different-length lists? zip stops at the shorter one, quietly.

Truthiness - empty things are False

What it actually is. Python lets you test collections directly: an empty list, string, dict, 0, and None are all "falsy"; non-empty ones are "truthy." So write if items:, not if len(items) > 0:.

items = []
if items:
    print("has stuff")
else:
    print("empty")            # this runs - [] is falsy
$ python truthy.py
empty

What just happened: The empty list counted as False, so else ran. if items: reads as "if there are any items," exactly what you mean. ⚠️ One catch: it can't tell None (missing) apart from [] (present but empty), or 0 from absent. When that matters, test explicitly: if value is not None:.

Context managers - with for guaranteed cleanup

You met with open(...) in Phase 7. The idiom generalizes: whenever something must be set up and reliably torn down - a file, a network connection, a lock - with is the Pythonic way, since cleanup happens even if an exception fires inside.

The gotcha cheat-card

Read these once and you'll dodge an afternoon of confusion each. They bite nearly everyone.

The gotcha What actually happens The fix
Mutable default argument - def f(x, items=[]): The [] is created once, at function definition, and shared across every call - it accumulates between calls. Default to None, build inside: def f(x, items=None): items = items or []
Late-binding closures - functions made in a loop all see the final loop value The closure captures the variable, not its value at creation time - all read i after the loop ended. Bind it as a default arg: lambda i=i: ..., or use a factory function
is vs == - a is b for comparing values is checks "same object in memory," not "equal value" - works by accident for small ints/None, fails on bigger values. Use == for value equality; reserve is for is None / is True
Integer caching - a is b "works" for 256 but not 257 CPython pre-caches small ints (−5 to 256) as shared objects, so is happens to be True for them; above that, separate objects. Same fix: never use is to compare numbers - use ==
The GIL - threads don't speed up CPU work CPython's Global Interpreter Lock lets only one thread run Python bytecode at a time, so CPU-bound threads never run in parallel. Use multiprocessing (or a native lib) for CPU work; threads suit I/O-bound waiting
Shadowing stdlib names - naming a file random.py or a variable list random.py shadows the standard library's, so import random imports your file; list = [...] hides the built-in list(). Don't name files/variables after stdlib modules or built-ins

Two of these deserve seeing in code:

Mutable default argument:

def add_item(item, basket=[]):     # the trap
    basket.append(item)
    return basket

print(add_item("a"))
print(add_item("b"))               # surprise: "a" is still here
$ python default.py
['a']
['a', 'b']

What just happened: The default [] was created once at definition time and reused on every call omitting basket, so the second call appended to the same list, which still held "a". Fix: default to None and create a fresh list inside the function each call.

is vs ==:

a = int("257")     # built at runtime, not a shared literal
b = int("257")
print(a == b)      # equal value?  yes
print(a is b)      # same object?  no (above the cached range)
$ python identity.py
True
False

What just happened: a and b hold equal values, so == is True. But they're separate integer objects in memory (257 is past CPython's small-int cache), so is - "the same object?" - is False. We built them with int("257") on purpose: two bare 257 literals in one file get folded into a single shared object by the compiler, so is would sneakily report True there - one more reason never to trust is on numbers. Use == for values, is for None/True/False.

💡 Key point. Almost every gotcha here comes from one of two confusions: when something is created (default args, closures - timing), or what equality means (is vs == - identity vs value). Hold those two distinctions and the surprises mostly evaporate.

Recap

  1. Comprehensions build lists/dicts in one readable expression; keep them simple.
  2. Unpacking (x, y = point, first, *rest = ...) pulls sequences apart by name.
  3. enumerate gives index+item; zip walks sequences in lockstep (stopping at the shortest).
  4. Truthiness lets you write if items: - test is not None when empty and missing differ.
  5. with (context managers) guarantees cleanup, exceptions or not.
  6. The gotcha cheat-card - mutable defaults, late-binding closures, is vs ==, integer caching, the GIL, shadowing stdlib names - are the surprises that bite everyone once. Now they won't bite you.

That's the basics done - phases 1-9. From here the guide goes deeper, into how Python actually works under your code, starting with its object model.


← Phase 8: The Ecosystem & Tooling · Guide overview · Phase 10: The Data Model & Dunder Methods →

Before the quiz: without looking back, say (or jot down) the core idea of this phase in your own words.

Check your understanding 3 questions

1. What is a comprehension?

2. Why use `==` rather than `is` to compare values?

3. What does Python's GIL mean for threads?