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.
=
# the long way
=
# the idiom
=
# with a filter
=
# a dict comprehension
=
$ 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
=
, = # unpack a tuple into two names
, * = # * grabs "everything else"
$ 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.
=
=
# index + value, no counter needed
# two lists, paired up
$ 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:.
=
# 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:
# the trap
return
# 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 ==:
= # built at runtime, not a shared literal
=
# equal value? yes
# 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 (
isvs==- identity vs value). Hold those two distinctions and the surprises mostly evaporate.
Recap
- Comprehensions build lists/dicts in one readable expression; keep them simple.
- Unpacking (
x, y = point,first, *rest = ...) pulls sequences apart by name. enumerategives index+item;zipwalks sequences in lockstep (stopping at the shortest).- Truthiness lets you write
if items:- testis not Nonewhen empty and missing differ. with(context managers) guarantees cleanup, exceptions or not.- The gotcha cheat-card - mutable defaults, late-binding closures,
isvs==, 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?