Question

What the yield Keyword Does in Python

pythoniteratorgeneratoryield

Question

In Python, what functionality does the yield keyword provide?

For example, I am trying to understand this code:

def _get_child_candidates(self, distance, min_dist, max_dist):
    if self._leftchild and distance - max_dist < self._median:
        yield self._leftchild
    if self._rightchild and distance + max_dist >= self._median:
        yield self._rightchild

And this is the caller:

result, candidates = [], [self]
while candidates:
    node = candidates.pop()
    distance = node._get_dist(obj)
    if distance <= max_dist and distance >= min_dist:
        result.extend(node._values)
    candidates.extend(node._get_child_candidates(distance, min_dist, max_dist))
return result

What happens when the method _get_child_candidates is called?

  • Is a list returned?
  • Is a single element returned?
  • Is the function called again?
  • When do subsequent calls stop?

I want to understand exactly how yield behaves in this example and how the caller uses the returned value.

Short Answer

By the end of this page, you will understand that yield turns a Python function into a generator function. Calling it does not run the whole function immediately and does not return a normal list. Instead, it returns a generator object that produces values one at a time as iteration happens. You will also see how iteration pauses at each yield, resumes on the next request, and stops automatically when the function finishes.

Concept

In Python, yield is used to produce a value from a function without finishing the function permanently. Any function that contains yield becomes a generator function.

When you call a generator function:

  • Python does not execute the whole function immediately.
  • It returns a generator object.
  • That generator can be iterated over with tools like:
    • for loops
    • list(...)
    • next(...)
    • methods like extend(...)

Each time iteration asks for a value:

  1. The function runs until it reaches yield.
  2. The yielded value is sent back to the caller.
  3. The function is paused, keeping its local variables and current position.
  4. On the next iteration request, the function resumes from where it paused.
  5. When the function reaches the end, iteration stops.

This matters because generators are:

  • memory-efficient: they produce values one at a time instead of building a full list first
  • lazy: values are computed only when needed
  • useful for streaming or traversal: especially when walking trees, reading files, or processing large datasets

In your example, _get_child_candidates(...) does not return a list. It returns a generator that may yield:

  • zero child nodes
  • one child node
  • two child nodes

The caller uses candidates.extend(...), which accepts any iterable. Since a generator is iterable, extend(...) pulls each yielded child from the generator and appends it to the candidates list.

Mental Model

Think of a generator like a bookmark inside a function.

A normal return is like closing the book and handing back the final result.

A yield is like saying:

"Here is one value for now. Keep my place open. I may have more later."

When the caller asks again, Python opens the book at the bookmark and continues reading until the next yield or the end.

For your tree example, _get_child_candidates(...) is like checking two doors:

  • If the left door is valid, hand it out.
  • Pause.
  • If the right door is valid, hand it out.
  • Pause.
  • Then stop.

So instead of building a list of doors first, the function hands them out one by one as needed.

Take Quiz

Syntax and Examples

The basic syntax is:

def my_generator():
    yield 1
    yield 2
    yield 3

Calling it does not give you a list:

g = my_generator()
print(g)

Output will look like this:

<generator object my_generator at 0x...>

To get values, you iterate:

for value in my_generator():
    print(value)

Output:

1
2
3

You can also convert it to a list:

print(list(my_generator()))

Output:

[1, 2, 3]

Example closer to your case

def get_child_candidates(has_left, has_right):
    if has_left:
        yield "left child"
    if has_right:
        yield "right child"

Usage:

print(list(get_child_candidates(True, True)))
print(list(get_child_candidates(True, False)))
print(list(get_child_candidates(False, False)))

Output:

['left child', 'right child']
['left child']
[]

This shows that the generator may yield multiple values, one value, or no values at all.

Your example explained simply

def _get_child_candidates(self, distance, min_dist, max_dist):
    if self._leftchild and distance - max_dist < self._median:
        yield self._leftchild
    if self._rightchild and distance + max_dist >= self._median:
        yield self._rightchild

This means:

  • maybe yield the left child
  • maybe yield the right child
  • stop when done

And this line:

candidates.extend(node._get_child_candidates(distance, min_dist, max_dist))

means:

  • call _get_child_candidates(...)
  • get a generator object
  • iterate through everything it yields
  • append each yielded child into candidates

So extend(...) consumes the generator.

Step by Step Execution

Consider this simplified version:

def get_items():
    print("Start")
    yield "A"
    print("Middle")
    yield "B"
    print("End")

Now trace it:

g = get_items()

At this moment:

  • the function body has not fully run
  • g is a generator object
  • nothing has been yielded yet

Next:

first = next(g)

What happens:

  1. The function starts running.
  2. print("Start") runs.
  3. It reaches yield "A".
  4. "A" is returned to the caller.
  5. The function pauses there.

Now:

  • first == "A"
  • the function has not finished

Next:

second = next(g)

What happens:

  1. The function resumes after yield "A".
  2. print("Middle") runs.
  3. It reaches yield "B".
  4. "B" is returned.
  5. The function pauses again.

Next:

next(g)

What happens:

  1. The function resumes after yield "B".
  2. print("End") runs.
  3. The function reaches the end.
  4. Python raises StopIteration to signal there are no more values.

Applying that to your code

When this runs:

node._get_child_candidates(distance, min_dist, max_dist)

Python returns a generator.

When candidates.extend(...) consumes that generator:

  1. The function starts.
  2. If the left-child condition is true, it yields self._leftchild.
  3. Iteration resumes.
  4. If the right-child condition is true, it yields self._rightchild.
  5. The function ends.
  6. Iteration stops automatically.

So the generator may produce 0, 1, or 2 child nodes depending on the conditions.

Real World Use Cases

Generators and yield are common in real Python programs when values should be produced gradually.

Tree and graph traversal

Your example is exactly this kind of use case.

  • walking nodes in a tree
  • finding neighboring nodes
  • exploring paths lazily

Reading large files

def read_lines(path):
    with open(path) as f:
        for line in f:
            yield line.strip()

This avoids loading the whole file into memory.

Processing API or database results

A program can yield records one by one instead of collecting everything first.

Data pipelines

Generators are often chained together:

def numbers():
    for i in range(10):
        yield i

def evens(values):
    for v in values:
        if v % 2 == 0:
            yield v

Infinite sequences

Generators can represent streams that never end unless the caller stops.

def counter():
    n = 0
    while True:
        yield n
        n += 1
Take Quiz

Real Codebase Usage

In real codebases, developers use generators for clarity, performance, and composability.

Lazy traversal

Instead of building temporary lists, code yields items as they are discovered.

def walk_nodes(node):
    if node.left:
        yield node.left
    if node.right:
        yield node.right

Filtering during iteration

A generator can apply conditions before yielding values.

def valid_users(users):
    for user in users:
        if user.is_active:
            yield user

Validation and guard-style skipping

Generators often combine simple checks with early skipping.

def non_empty_lines(lines):
    for line in lines:
        line = line.strip()
        if not line:
            continue
        yield line

Replacing temporary lists

Instead of this:

def get_ids(users):
    result = []
    for user in users:
        result.append(user.id)
    return result

Developers may use:

def get_ids(users):
    for user in users:
        yield user.id

This is helpful when the caller only needs some values, not all.

Interoperating with iterable-based APIs

Many Python APIs accept any iterable, not just lists. Common examples include:

  • list(...)
  • sum(...)
  • any(...)
  • all(...)
  • set(...)
  • dict(...) in some forms
  • list methods like extend(...)

That is why your generator works directly with candidates.extend(...).

Common Mistakes

Mistake 1: Thinking yield returns a list

Broken expectation:

def numbers():
    yield 1
    yield 2

x = numbers()
print(x[0])

This fails because x is a generator, not a list.

Use one of these instead:

x = list(numbers())
print(x[0])

or iterate:

for n in numbers():
    print(n)

Mistake 2: Confusing yield with return

def example():
    yield 1
    yield 2

This produces multiple values over time.

But:

def example():
    return 1
    return 2

The second return is never reached.

Mistake 3: Forgetting that generators are consumed

g = (x for x in range(3))
print(list(g))
print(list(g))

Output:

[0, 1, 2]
[]

Once a generator is exhausted, it does not automatically restart.

Mistake 4: Expecting code after yield to run immediately

def demo():
    yield 1
    print("after yield")

The print(...) runs only when the next value is requested or when iteration continues.

Mistake 5: Using yield when a simple return is clearer

If a function naturally computes one result and finishes, return is often simpler.

Use yield when:

  • there are multiple values
  • values should be produced lazily
  • you want iteration behavior

Mistake 6: Assuming a generator always yields something

def maybe_items(flag):
    if flag:
        yield "item"

If flag is False, this yields nothing. That is valid. Iteration simply ends immediately.

Comparisons

ConceptWhat it doesProduces valuesMemory behaviorTypical use
returnEnds the function and sends back one final resultOnceUsually computes full result before returningSimple functions
yieldPauses the function and sends back one value at a timeMultiple timesLazy, often memory-efficientIteration, streaming, traversal
ListStores all items immediatelyAll at onceUses memory for all itemsSmall to medium collections, indexing
GeneratorProduces items on demandOne at a timeUsually lower memory usageLarge data, pipelines, traversal

yield vs return

def with_return():
    return [1, 2, 3]

def with_yield():
    yield 1
    yield 2
    yield 3
  • with_return() gives a list immediately.
  • with_yield() gives a generator.

Generator vs list in your example

A list-based version could look like this:

def _get_child_candidates(self, distance, min_dist, max_dist):
    result = []
    if self._leftchild and distance - max_dist < self._median:
        result.append(self._leftchild)
    if self._rightchild and distance + max_dist >= self._median:
        result.append(self._rightchild)
    return result

This also works, but yield avoids creating the list explicitly and fits naturally with iteration.

Take Quiz

Cheat Sheet

# A function with yield is a generator function

def gen():
    yield 1
    yield 2

Key rules

  • Calling a generator function returns a generator object.
  • The function body runs when iteration begins, not all at once.
  • Each yield produces one value and pauses the function.
  • Local state is preserved between yields.
  • When the function ends, iteration stops with StopIteration.
  • A generator can yield zero, one, or many values.
  • Generators are iterable but are not lists.
  • Once consumed, a generator is exhausted.

Common ways to consume a generator

g = gen()
next(g)
list(gen())
for x in gen():
    print(x)

In your example

candidates.extend(node._get_child_candidates(distance, min_dist, max_dist))

This means:

  • call the generator function
  • iterate over each yielded child
  • append each child to candidates

Generator may produce

  • 0 values
  • 1 value
  • 2 values

for your _get_child_candidates(...) function.

Mental shortcut

  • return = done forever
  • yield = here is one value, pause, continue later

FAQ

Does yield return a list in Python?

No. It returns a generator object. You can convert it to a list with list(generator) if needed.

What happens when a function with yield is called?

It returns a generator object immediately. The function body is then executed step by step as values are requested.

Can a generator yield more than one value?

Yes. A generator can yield any number of values, including none.

When does a generator stop?

It stops when the function reaches the end or hits a return. Python signals this with StopIteration internally.

Why use yield instead of building a list?

Because it can be more memory-efficient and lets you compute values only when needed.

Can I loop over a generator more than once?

Not the same generator object after it has been exhausted. You usually need to create a new generator by calling the generator function again.

Is yield only for large datasets?

No. It is also useful for clean iteration logic, tree traversal, pipelines, and readable code.

Take Quiz

Mini Project

Description

Build a small tree traversal helper that uses yield to return child nodes lazily. This mirrors the original question and helps you see how generator functions work in a realistic structure.

Goal

Create a function that yields available child nodes one at a time and then collect them using normal iteration.

Requirements

  • Create a simple Node class with optional left and right children.
  • Add a method that uses yield to produce existing child nodes.
  • Traverse a node and collect its children into a list.
  • Show that the generator can yield zero, one, or two values.
Take Quiz

Keep learning

Related questions

View more