A generator produces values one at a time instead of building a whole list. Any function with yield in it's a generator function: calling it returns a generator, and each yield hands out one value and pauses until the next one is asked for.
(x * 2 for x in data) is a generator expression, the lazy cousin of a list comprehension. Generators are ideal for big files and streams: you process a line, then the next, without holding everything in memory.
next(gen) takes the next value and raises StopIteration at the end; next(gen, None) returns None instead. That makes "first match or nothing" one line.
A generator can be looped over once. After sum(gen), it's exhausted and gives nothing more, so make a list if you need the values twice.
Generators can be endless, like a stream of invoice numbers. Take what you need with itertools.islice.