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:
forloopslist(...)next(...)- methods like
extend(...)
Each time iteration asks for a value:
- The function runs until it reaches
yield. - The yielded value is sent back to the caller.
- The function is paused, keeping its local variables and current position.
- On the next iteration request, the function resumes from where it paused.
- 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.
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
gis a generator object- nothing has been yielded yet
Next:
first = next(g)
What happens:
- The function starts running.
print("Start")runs.- It reaches
yield "A". "A"is returned to the caller.- The function pauses there.
Now:
first == "A"- the function has not finished
Next:
second = next(g)
What happens:
- The function resumes after
yield "A". print("Middle")runs.- It reaches
yield "B". "B"is returned.- The function pauses again.
Next:
next(g)
What happens:
- The function resumes after
yield "B". print("End")runs.- The function reaches the end.
- Python raises
StopIterationto 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:
- The function starts.
- If the left-child condition is true, it yields
self._leftchild. - Iteration resumes.
- If the right-child condition is true, it yields
self._rightchild. - The function ends.
- 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
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
| Concept | What it does | Produces values | Memory behavior | Typical use |
|---|---|---|---|---|
return | Ends the function and sends back one final result | Once | Usually computes full result before returning | Simple functions |
yield | Pauses the function and sends back one value at a time | Multiple times | Lazy, often memory-efficient | Iteration, streaming, traversal |
| List | Stores all items immediately | All at once | Uses memory for all items | Small to medium collections, indexing |
| Generator | Produces items on demand | One at a time | Usually lower memory usage | Large 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.
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
yieldproduces 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
0values1value2values
for your _get_child_candidates(...) function.
Mental shortcut
return= done foreveryield= 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.
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
Nodeclass with optionalleftandrightchildren. - Add a method that uses
yieldto 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.
Keep learning
Related questions
@staticmethod vs @classmethod in Python Explained
Learn the difference between @staticmethod and @classmethod in Python with clear examples, use cases, mistakes, and a mini project.
Add Rows to a Pandas DataFrame in Python
Learn how to add rows to a Pandas DataFrame, why repeated row appends are slow, and when to use loc, concat, or record lists.
Call a Function by Name in a Python Module
Learn how to call a function by name in a Python module using strings, getattr, and safe patterns for dynamic function dispatch.