Gemini Structured Output With Zod in Next.js
Describe the JSON once in Zod, send z.toJSONSchema() to Gemini as responseJsonSchema, then validate the reply with safeParse and retry once with the errors.
A Python list comprehension builds a new list in one line by writing [expression for item in iterable], with an optional if at the end to filter items. It replaces the "make an empty list, loop, append" pattern. Below are the list comprehension examples I actually use, from the basic form to filtering, if/else, nested loops and the mistakes that tripped me up.
When I started learning Python, comprehensions looked like a puzzle. Once I learned to read them out loud, they became one of my favourite parts of the language. Every example here was run with Python 3.13, and the output in the comments at the bottom of each block is the real output.
Here is the same list of squares built two ways:
# The classic loop way
squares = []
for n in range(1, 6):
squares.append(n * n)
print(squares)
# The same thing as a list comprehension
squares = [n * n for n in range(1, 6)]
print(squares)
# Output:
# [1, 4, 9, 16, 25]
# [1, 4, 9, 16, 25]
The comprehension has three parts inside square brackets:
n * n): what goes into the new list.for n in range(1, 6)): where the items come from.if ...): which items to keep.The trick that helped me is to read it in plain English: "give me n * n for every n in 1 to 5." The result is always a brand new list. The original iterable is not changed.
Put an if after the for clause and only the items that pass the test make it into the new list. This is the comprehension I write most often, usually to clean data before using it.
marks = [72, 45, 88, 39, 91, 60, 33]
# Keep only the passing marks (40 or more)
passed = [m for m in marks if m >= 40]
print(passed)
# Clean up messy user input: strip spaces, drop empty strings
raw_tags = [" python ", "", "html", " ", " css"]
tags = [t.strip().lower() for t in raw_tags if t.strip()]
print(tags)
# Output:
# [72, 45, 88, 91, 60]
# ['python', 'html', 'css']
The second example does two jobs at once. The condition if t.strip() drops strings that are empty or only spaces, because an empty string counts as false. The expression t.strip().lower() then tidies up the ones that are left.
This is the part that confused me the most. There are two different places a condition can go, and they do different things:
marks = [72, 45, 88, 39, 91]
# if/else goes BEFORE the for, because it decides the value
results = ["pass" if m >= 40 else "fail" for m in marks]
print(results)
# A filter goes AFTER the for, because it decides whether to keep the item
high = [m for m in marks if m >= 80]
print(high)
# Output:
# ['pass', 'pass', 'pass', 'fail', 'pass']
# [88, 91]
x if condition else y before the for is a conditional expression. It runs for every item and decides what value goes in. The new list is always the same length as the original.if condition after the for is a filter. It decides whether an item goes in at all, so the new list can be shorter.If you write [m for m in marks if m >= 40 else 0], Python raises a SyntaxError, because a filter can't have an else. Move the whole if/else to the front instead.
Practice lists of numbers are fine, but most of the time I'm working with a list of dictionaries, such as rows from a CSV file or a JSON response from an API. Comprehensions are great for pulling out exactly the fields you need.
students = [
{"name": "Rafi", "mark": 72},
{"name": "Nusrat", "mark": 88},
{"name": "Tanvir", "mark": 39},
]
names = [s["name"] for s in students]
print(names)
toppers = [s["name"] for s in students if s["mark"] >= 70]
print(toppers)
# Pair two lists together with zip()
subjects = ["Math", "Physics", "Chemistry"]
scores = [81, 67, 74]
report = [f"{sub}: {score}" for sub, score in zip(subjects, scores)]
print(report)
# Output:
# ['Rafi', 'Nusrat', 'Tanvir']
# ['Rafi', 'Nusrat']
# ['Math: 81', 'Physics: 67', 'Chemistry: 74']
The zip() example shows that the for clause can unpack values, just like a normal loop. for sub, score in zip(subjects, scores) walks through both lists side by side. Mixing in an f-string in the expression is a common way to build labels or lines of a report.
You can use more than one for in a comprehension, and you can put a comprehension inside another one. These are powerful, but they are also where readability goes downhill fast.
grid = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
# Flatten: read the for clauses left to right, like nested loops
flat = [n for row in grid for n in row]
print(flat)
# Build a 3x4 table of zeros (a list of lists)
table = [[0 for col in range(4)] for row in range(3)]
print(table)
# Transpose rows into columns
columns = [[row[i] for row in grid] for i in range(3)]
print(columns)
# Output:
# [1, 2, 3, 4, 5, 6, 7, 8, 9]
# [[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]]
# [[1, 4, 7], [2, 5, 8], [3, 6, 9]]
For the flatten example, read the for clauses in the same order you would write nested loops: first for row in grid, then for n in row. The table and columns examples are different: there the inner comprehension is the expression, so each item of the outer list is itself a new list.
My rule: if I need more than two for clauses, or a nested comprehension plus a condition, I write a normal loop. Code is read more often than it is written.
The same syntax works with other brackets. Curly braces with a key: value pair give you a dictionary, and curly braces with a single value give you a set. Round brackets give you a generator expression, which produces values one at a time instead of building a whole list in memory.
words = ["apple", "banana", "avocado", "cherry", "apple"]
# Dict comprehension: word -> length
lengths = {w: len(w) for w in words}
print(lengths)
# Set comprehension: unique first letters
first_letters = {w[0] for w in words}
print(sorted(first_letters))
# Generator expression: no list is built, sum() pulls values one by one
total_chars = sum(len(w) for w in words)
print(total_chars)
# Output:
# {'apple': 5, 'banana': 6, 'avocado': 7, 'cherry': 6}
# ['a', 'b', 'c']
# 29
Notice that "apple" appears twice in the input but only once in the dictionary, because a later key overwrites an earlier one. I sorted the set before printing, since sets have no guaranteed order. When you only need to pass the values into a function like sum(), max(), any() or "".join(), a generator expression is enough, and you can drop the extra brackets.
Sometimes the filter and the expression need the same calculated value. Since Python 3.8, the assignment expression := lets you compute it once and reuse it:
readings = ["12", "x", "7", "", "30"]
def to_int(text):
try:
return int(text)
except ValueError:
return None
# := stores the converted value so to_int() runs only once per item
numbers = [n for r in readings if (n := to_int(r)) is not None]
print(numbers)
# Output:
# [12, 7, 30]
Without :=, you would call to_int() twice per item, once in the filter and once in the expression. It's a handy tool, but use it lightly, because it makes the line harder to read for beginners.
As covered above, if alone goes at the end, while if ... else ... goes at the front. When you get a SyntaxError in a comprehension, check this first.
A comprehension is for building a list. If you only want to do something for each item, like printing or saving to a file, use a normal loop:
names = ["rafi", "nusrat"]
# Don't do this: a list of None values built only for the side effect
result = [print(n.title()) for n in names]
print(result)
# Do this instead
for n in names:
print(n.title())
# Output:
# Rafi
# Nusrat
# [None, None]
# Rafi
# Nusrat
The first version works, but it also builds a useless list of None values, because print() returns None. Anyone reading it will wonder what result is for.
In Python 3, a comprehension has its own scope, so its loop variable does not overwrite a variable with the same name outside it:
x = "outer"
values = [x for x in range(3)]
print(values)
print(x) # the loop variable does not leak out in Python 3
# Output:
# [0, 1, 2]
# outer
This is a good thing, but it means you can't use the loop variable after the comprehension ends, the way you can after a normal for loop.
A comprehension that runs past the edge of the screen, with function calls, conditions and nested loops, is not clever. It's just hard to debug. Split long comprehensions over several lines inside the brackets, or switch to a loop.
sum([x * x for x in range(1_000_000)]) creates a million-item list first. sum(x * x for x in range(1_000_000)) gives the same answer without storing the list.
For simple cases they are usually a bit faster than a loop that calls append(), because there is less work per item. The difference is rarely what matters, though. Choose a comprehension because it's clearer for building a list, not to save microseconds.
There is no elif keyword inside a comprehension, but you can chain conditional expressions: ["A" if m >= 80 else "B" if m >= 60 else "C" for m in marks]. It works, but past two conditions I move the logic into a small function and call that function in the expression.
map(func, items) applies a function to every item and returns a lazy iterator. A list comprehension does the same job and can filter at the same time, and many Python developers find it easier to read. Both are fine. I mostly use comprehensions.
Yes. Any iterable works: strings give you characters, range() gives you numbers, and an open file gives you lines, so [line.strip() for line in f] is a common way to read a file into a clean list.
A list comprehension is a short way to say "build a new list from this iterable, transform each item, and optionally skip some". Start with the basic form, add a filter at the end when you need one, and remember that if/else goes at the front. When a comprehension gets hard to read, a plain loop is always allowed. If you also write JavaScript, the same ideas exist there as map() and filter().
The official Python tutorial has a good section on list comprehensions with more examples if you want to go deeper. I'm still learning too, and you can see what I've been building on my projects page.
// note
This is a learning note from studying the web. It is one small topic, written so I can remember it. It is not a course and not a claim that I have finished the subject.
If a sentence is wrong, say so from the contact page and name this title. Drafts never appear here. Related notes, when they exist, are other published posts, and the same sample rule applies to each of them.
Describe the JSON once in Zod, send z.toJSONSchema() to Gemini as responseJsonSchema, then validate the reply with safeParse and retry once with the errors.
Step by step: a Gemini key from Google AI Studio in .env.local, a server-only helper, a Next.js route handler, Vercel environment variables and a leak test.
A small offline habit tracker in plain HTML and JavaScript: habits saved as JSON in localStorage, streaks from local dates, a 7-day row and a JSON backup.