Python Basics

Comprehensions

Comprehensions

A comprehension builds a list or a dictionary in a single expression. It is the most distinctively Python thing in this tutorial, it appears constantly in framework code, and there is no clean equivalent in Java or C# unless you count streams and LINQ.

The pattern

The long way:

names = ["priya", "arun", "meera"]

capitalised = []
for name in names:
    capitalised.append(name.title())

print(capitalised)

The comprehension:

capitalised = [name.title() for name in names]

Same result, one line. Read it left to right as a sentence:

"give me name.title(), for every name in names"

The structure is always:

[ what to produce   for   each item   in   the source ]

The thing you want comes first, the loop comes after. That inversion is what makes it look strange for a week and obvious afterwards.

Filtering

Add an if on the end to keep only some items.

tickets = ["P1", "P3", "P2", "P1", "P4"]

urgent = [t for t in tickets if t == "P1"]
print(urgent)

Read it as: "give me t, for every t in tickets, where t is P1".

You can transform and filter at once:

lengths = [len(t) for t in tickets if t != "P4"]

Dictionary comprehensions

Same idea, curly braces, and a key: value pair.

members = ["priya", "arun", "meera"]

lookup = {name: len(name) for name in members}
print(lookup)
{'priya': 5, 'arun': 4, 'meera': 5}

A pattern you will use often — turning a list of records into a lookup by id:

tickets = [
    {"id": 1, "severity": "P1"},
    {"id": 2, "severity": "P3"},
]

by_id = {t["id"]: t for t in tickets}
print(by_id[2])

That replaces a loop and gives you instant lookup by key.

Inverting a dictionary is a two-liner people often write as ten:

codes = {"critical": "P1", "high": "P2"}
flipped = {v: k for k, v in codes.items()}

Set comprehensions

Curly braces with no colon gives a set, which removes duplicates for you.

owners = {t["owner"] for t in tickets}

Where you will meet them in agent code

They are everywhere, usually doing one of three jobs.

Extracting one field from a list of records:

contents = [m["content"] for m in messages]

Building the arguments for a fan-out:

results = await asyncio.gather(*[summarise(d) for d in docs])

Turning your tool functions into the list a framework wants:

tools = [tool(f) for f in [get_weather, search_web, lookup_order]]

Recognising the shape is most of the benefit. Once [x for y in z] reads as one idea rather than three, framework source stops looking dense.

Generator expressions

Swap the square brackets for round ones and you get a generator — values produced one at a time instead of a list built in memory.

squares_list = [n * n for n in range(1_000_000)]
squares_gen = (n * n for n in range(1_000_000))

Use round brackets when you only intend to loop once, especially with sum, any, all or max:

total = sum(len(line) for line in read_lines("huge.log"))
has_p1 = any(t == "P1" for t in tickets)

When not to use one

A comprehension is for one simple transformation. The moment it needs a second thought, write the loop.

Do not write this:

result = [transform(x) if check(x) else fallback(x) for x in items if x is not None and x.enabled]

Write this:

result = []
for x in items:
    if x is None or not x.enabled:
        continue
    result.append(transform(x) if check(x) else fallback(x))

The second is longer and better. Nobody has ever been thanked for a clever one-liner they had to explain.

Two rules that hold up well:

  • if it does not fit comfortably on one line, use a loop
  • if it has more than one for, use a loop

Coming from Java or C#

You have met this idea, just spelled very differently.

TaskJavaC#Python
Transform.stream().map(f).toList().Select(f).ToList()[f(x) for x in xs]
Filter.stream().filter(p).toList().Where(p).ToList()[x for x in xs if p(x)]
Both.filter(p).map(f).Where(p).Select(f)[f(x) for x in xs if p(x)]
To a mapCollectors.toMap(k, v).ToDictionary(k, v){k(x): v(x) for x in xs}
LazyStreamIEnumerable(f(x) for x in xs)

The one habit to adjust: LINQ and streams read in pipeline order — source first, then operations. A comprehension puts the result first and the source second. Read the middle of the line to find out what it is looping over.

Common mistakes

  • Square brackets when you wanted a generator, building a huge list you only walk once.
  • Cramming in too much. If you paused to work out what it does, so will everyone else.
  • Forgetting .items() when looping a dictionary: for k, v in d.items(), not for k, v in d.
  • Using one for side effects. [print(x) for x in xs] builds a throwaway list of None. Use a plain for loop when you want an action, not a value.

Practise this

  • Turn a list of prices into a list of prices with 18% tax added.
  • From a list of ticket dictionaries, build a list of the ids of the P1 ones only.
  • Build a lookup dictionary keyed by id from a list of records.
  • Rewrite one of your comprehensions as a plain loop, and decide honestly which reads better.

Try It Yourself

Copy this into a new Colab notebook and run it. Nothing to install.

example.py
1# Comprehensions: the result comes FIRST, the loop comes after.
2
3names = ["priya", "arun", "meera"]
4
5# The long way
6capitalised = []
7for name in names:
8 capitalised.append(name.title())
9print(capitalised)
10
11# The comprehension - same thing
12print([name.title() for name in names])
13
14# Filtering: add `if` on the end
15tickets = ["P1", "P3", "P2", "P1", "P4"]
16print([t for t in tickets if t == "P1"])
17
18# Transform AND filter
19print([len(t) for t in tickets if t != "P4"])
20
21# Dictionary comprehension
22print({name: len(name) for name in names})
23
24# The pattern you will use most: records -> lookup by id
25records = [
26 {"id": 1, "severity": "P1", "owner": None},
27 {"id": 2, "severity": "P3", "owner": "network"},
28 {"id": 3, "severity": "P1", "owner": "database"},
29]
30by_id = {r["id"]: r for r in records}
31print(by_id[2])
32
33# Pull one field out of a list of dictionaries
34print([r["severity"] for r in records])
35
36# Inverting a dictionary
37codes = {"critical": "P1", "high": "P2"}
38print({v: k for k, v in codes.items()})
39
40# Set comprehension - duplicates removed for you
41print({r["severity"] for r in records})
42
43# Generator expression - round brackets, one value at a time
44squares = (n * n for n in range(1_000_000))
45print("a generator, not a list:", squares)
46print("sum of first 10 squares:", sum(n * n for n in range(10)))
47print("any P1?", any(r["severity"] == "P1" for r in records))
48
49# When NOT to use one. This is legal and unkind:
50bad = [r["id"] if r["owner"] else -r["id"] for r in records if r["severity"] == "P1"]
51print(bad)
52
53# The same thing, readable:
54good = []
55for r in records:
56 if r["severity"] != "P1":
57 continue
58 good.append(r["id"] if r["owner"] else -r["id"])
59print(good)