If you have ever called .values() on a Python dictionary, you have used one of the language's most quietly revolutionary features. On the surface, dict.values() looks simple — it gives you the values in a dictionary. But beneath that simplicity lies a design decision shaped by years of debate, multiple Python Enhancement Proposals, a complete reimplementation of the dictionary data structure, and a philosophical shift in how Python thinks about data access.
This article takes you from practical usage through advanced patterns to the deep internals, covering every aspect of dict.values() that a Python developer should understand.
What dict.values() Actually Returns
The first thing to understand is what dict.values() does not return: a list. In Python 3, calling .values() on a dictionary returns a dict_values object, which is a view into the dictionary's data. This is not a copy. It is a live, dynamic window into the dictionary that reflects changes in real time.
prices = {"apple": 1.20, "banana": 0.50, "cherry": 2.75}
vals = prices.values()
print(type(vals))
# <class 'dict_values'>
print(vals)
# dict_values([1.2, 0.5, 2.75])
# Modify the dictionary
prices["banana"] = 0.75
prices["mango"] = 3.00
# The view automatically reflects the changes
print(vals)
# dict_values([1.2, 0.75, 2.75, 3.0])
When you store the result of dict.values() in a variable, you are not taking a snapshot — you are holding a reference that always shows the current state of the dictionary. This has profound implications for both performance and correctness.
The Python 2 vs. Python 3 Split
In Python 2, dict.values() returned a plain list — a full copy of all the values in the dictionary. If your dictionary had a million entries, calling .values() allocated a new list with a million references. The same was true for .keys() and .items(). Python 2 also provided .itervalues(), .iterkeys(), and .iteritems(), which returned lazy iterators that avoided the memory cost of building a full list.
Python 3 eliminated this split entirely. The .iter*() methods were removed. The .values(), .keys(), and .items() methods were redesigned to return view objects instead of lists. This change was formalized in PEP 3106 — "Revamping dict.keys(), .values() and .items()", authored by Guido van Rossum.
PEP 3106 explains that the original plan was simply to make these methods return iterators, matching what the .iter*() methods did in Python 2. However, the PEP notes that a better solution was possible, inspired by the Java Collections Framework: the methods return objects with set behavior (for .keys() and .items()) or multiset behavior (for .values()) that do not contain copies but rather reference the underlying dict and pull their values out as needed.
The advantage is that you can still iterate over the view multiple times:
a = d.items()
for k, v in a:
...
# And later, again:
for k, v in a:
...
With a simple iterator, the second loop would produce nothing because the iterator would already be exhausted. With a view object, you can iterate over it as many times as you want.
What You Can (and Cannot) Do with dict_values
The dict_values object supports three core operations: you can measure its length with len(), test membership with in, and iterate over it with a for loop or by passing it to functions that consume iterables.
config = {"host": "localhost", "port": 8080, "debug": True}
vals = config.values()
# Length
len(vals) # 3
# Membership testing
8080 in vals # True
"localhost" in vals # True
# Iteration
for v in vals:
print(v)
# localhost
# 8080
# True
# Conversion to other types
list(vals) # ['localhost', 8080, True]
tuple(vals) # ('localhost', 8080, True)
You cannot write vals[0] to get the first value. A view is a proxy, not a sequence — it has no independent storage, so random access by index would need to traverse the dictionary's internal structure each time. If you need indexed access, convert the view to a list first.
The dict_values object also does not support set operations like union, intersection, or difference. This is a deliberate asymmetry: dict_keys does support set operations (because keys are guaranteed to be unique and hashable), and dict_items supports them when the values are hashable. But dict_values cannot, because values may contain duplicates and may include unhashable types like lists. PEP 3106 explicitly describes this: the values view behaves like a simpler unordered collection — it cannot be a set because duplicate values are possible.
d1 = {"a": 1, "b": 2, "c": 3}
d2 = {"b": 2, "c": 4, "d": 5}
# Keys support set operations
d1.keys() & d2.keys() # {'b', 'c'}
d1.keys() - d2.keys() # {'a'}
# Values do NOT support set operations
# d1.values() & d2.values() # TypeError!
# But you can convert to a set when values are hashable
set(d1.values()) & set(d2.values()) # {2}
The dict_values Object Is Not Hashable
Because the contents of a dict_values view can change whenever the underlying dictionary changes, the view itself cannot be hashed. PEP 3106's specification states that d_values.__hash__ should raise TypeError, since the view's contents can change if the underlying dict is mutated — making a stable hash value impossible. This means you cannot use a dict_values object as a dictionary key or add it to a set.
Iteration Order: A Guarantee Since Python 3.7
One of the questions that arises naturally with dict.values() is: in what order do the values appear? The answer depends on which version of Python you are using, and the history behind that answer is one of the most significant changes in the language's recent history.
Prior to Python 3.6, dictionaries had no guaranteed ordering. The order of iteration was an implementation detail that could change between Python versions, platforms, or even between runs of the same program.
In December 2012, Raymond Hettinger posted a proposal to the python-dev mailing list titled "More compact dictionaries with faster iteration." He observed that the existing dictionary implementation was unnecessarily inefficient, using a sparse table of 24-byte entries containing the hash value, key pointer, and value pointer. He proposed instead storing entries in a dense table referenced by a sparse table of indices. This idea, which also drew on prior work done in Java, was first implemented in PyPy by Maciej Fijałkowski and Armin Rigo, with Laurence Tratt credited for prodding the work to completion.
INADA Naoki implemented Hettinger's proposal for CPython, and it shipped in Python 3.6. The result was dictionaries using 20% to 25% less memory compared to Python 3.5, as noted in the Python 3.6 "What's New" documentation. A critical side effect of this new implementation was that dictionaries now preserved insertion order, because the dense entry table naturally stored entries in the order they were added. However, the Python 3.6 documentation was careful to note that the order-preserving aspect was considered an implementation detail and should not be relied upon.
The Python 3.7 "What's New" documentation made it official: the insertion-order preservation nature of dict objects has been declared to be an official part of the Python language spec. This means dict.values() is guaranteed to return values in the order their corresponding keys were inserted. It also means list(d.items()) == list(zip(d.keys(), d.values())) always holds true.
The RuntimeError Trap: Mutating During Iteration
One of the dangers of view objects is that they are tied to a live dictionary. If you modify the dictionary's size while iterating over a view, Python raises a RuntimeError.
scores = {"Alice": 85, "Bob": 62, "Charlie": 91, "Diana": 58}
# This will raise RuntimeError
for name, score in scores.items():
if score < 70:
del scores[name]
# RuntimeError: dictionary changed size during iteration
PEP 3106 addresses this directly: as in Python 2.x, mutating a dict while iterating over it using an iterator has an undefined effect and will in many cases raise a RuntimeError.
The standard workaround is to materialize the view into a list before iterating, creating a snapshot that is immune to changes:
scores = {"Alice": 85, "Bob": 62, "Charlie": 91, "Diana": 58}
# Safe: iterate over a snapshot
for name, score in list(scores.items()):
if score < 70:
del scores[name]
print(scores)
# {'Alice': 85, 'Charlie': 91}
Alternatively, in modern Python, you can build a new dictionary with a comprehension:
scores = {name: score for name, score in scores.items() if score >= 70}
Modifying values (without adding or removing keys) is safe during iteration and does not trigger a RuntimeError. Only changes that alter the dictionary's structure — insertions and deletions — will cause the error.
scores = {"Alice": 85, "Bob": 62, "Charlie": 91}
for name in scores:
scores[name] += 10 # Safe: not changing the dictionary's size
print(scores)
# {'Alice': 95, 'Bob': 72, 'Charlie': 101}
Performance: Why Views Beat Lists
The performance advantage of view objects is straightforward: they are essentially free to create. A dict_values view is just a lightweight wrapper that holds a reference to the underlying dictionary. It allocates no additional memory proportional to the dictionary's size. Creating a list from .values() requires allocating a new list and copying every reference, which is O(n) in both time and memory.
This matters in practice when you only need to iterate once, test membership, or pass the values to a function that accepts any iterable:
# No need to create a list -- the view is sufficient
total = sum(prices.values())
highest = max(prices.values())
has_zero = 0 in prices.values()
Membership testing on dict_values is O(n), because values are not hashed. If you need repeated lookups, converting to a set first is more efficient: create the set once in O(n), then each lookup is O(1).
# O(n) each time
"localhost" in config.values()
# O(n) once, then O(1) per lookup
value_set = set(config.values())
"localhost" in value_set
Common Patterns with dict.values()
Summing, averaging, and aggregating. The dict_values view works seamlessly with built-in aggregation functions:
grades = {"math": 92, "science": 88, "english": 95, "history": 79}
total = sum(grades.values()) # 354
average = sum(grades.values()) / len(grades) # 88.5
highest = max(grades.values()) # 95
lowest = min(grades.values()) # 79
Checking if all or any values meet a condition:
permissions = {"read": True, "write": False, "execute": True}
all(permissions.values()) # False (write is False)
any(permissions.values()) # True
Flattening nested values:
teams = {
"engineering": ["Alice", "Bob"],
"design": ["Charlie"],
"marketing": ["Diana", "Eve", "Frank"]
}
# Flatten all team members into a single list
all_members = [person for team in teams.values() for person in team]
# ['Alice', 'Bob', 'Charlie', 'Diana', 'Eve', 'Frank']
Finding keys by value (reverse lookup):
capitals = {"France": "Paris", "Japan": "Tokyo", "Brazil": "Brasilia"}
# Find the country whose capital is Tokyo
country = next(k for k, v in capitals.items() if v == "Tokyo")
# 'Japan'
Counting value frequencies:
from collections import Counter
responses = {"q1": "yes", "q2": "no", "q3": "yes", "q4": "yes", "q5": "no"}
Counter(responses.values())
# Counter({'yes': 3, 'no': 2})
Related PEPs and the Broader Context
PEP 3106 is the primary PEP, as discussed throughout this article. It transformed .keys(), .values(), and .items() from list-returning methods into view-returning methods.
PEP 468 — "Preserving the order of **kwargs in a function" is closely related because it depends on dictionary ordering. PEP 468 specified that keyword arguments collected via **kwargs should preserve their passed order. This was made possible by the compact dict implementation in Python 3.6, and it meant that kwargs.values() would yield values in the order the caller passed them.
PEP 509 — "Add a private version to dict" added an internal version counter (ma_version_tag) to dictionaries, incremented at each dictionary creation and modification. This mechanism was designed to implement fast guards on namespaces — allowing CPython to skip expensive dictionary lookups for global and builtin variables when the underlying namespace had not changed. It is worth noting that PEP 509 is not the mechanism behind the RuntimeError raised when you mutate a dict during iteration; that safety check uses a separate, older internal counter that tracks structural changes. PEP 509 was later superseded by PEP 699 (Python 3.12+), which declared the ma_version_tag field an internal implementation detail, as CPython's specializing interpreter had moved to other optimization strategies.
PEP 584 — "Add Union Operators To dict" (Python 3.9) added the | and |= operators for merging dictionaries. When you merge dictionaries using these operators, the resulting dictionary's .values() reflects the merge order: values from the right-hand operand override those from the left for duplicate keys, and the insertion order follows left-to-right processing.
defaults = {"color": "blue", "size": "medium", "theme": "light"}
overrides = {"size": "large", "theme": "dark"}
merged = defaults | overrides
list(merged.values())
# ['blue', 'large', 'dark']
dict.values() in Subclasses and the collections Module
The behavior of .values() is consistent across dictionary subclasses in the standard library. collections.OrderedDict, collections.defaultdict, and collections.Counter all return dict_values view objects when you call .values().
For Counter objects specifically, .values() returns the counts:
from collections import Counter
word_counts = Counter("mississippi")
list(word_counts.values())
# [1, 4, 4, 2] (counts for m, i, s, p)
The types.MappingProxyType, introduced in Python 3.3, provides a read-only view of a dictionary. Its .values() method also returns a view, and since the proxy prevents mutation, you never have to worry about the underlying dictionary changing out from under you.
When to Convert and When to Use the View Directly
The general rule is to use the dict_values view directly whenever possible, and convert only when you need a feature that views do not provide.
Use the view directly when iterating, passing to sum()/max()/min()/any()/all(), testing membership with in, or checking length with len(). Convert to a list when you need indexing, slicing, or a stable snapshot that won't change. Convert to a set when you need fast repeated membership testing or set operations on the values. Convert to a tuple when you need an immutable, hashable representation of the values at a point in time.
# Direct view usage (preferred)
for val in d.values():
process(val)
# Convert when you need indexing
vals_list = list(d.values())
first_value = vals_list[0]
# Convert when you need set operations
common = set(d1.values()) & set(d2.values())
Conclusion
dict.values() is deceptively simple. A single method call hides a story that spans more than a decade of Python's evolution — from the list-based approach of Python 2, through PEP 3106's introduction of view objects, to Raymond Hettinger's compact dictionary proposal, INADA Naoki's CPython implementation, and Guido van Rossum's decision to make insertion-order preservation an official language guarantee in Python 3.7.
- Views, not copies:
dict.values()returns a live view into the dictionary, not a list. It allocates no extra memory and always reflects the current state of the dict. - Insertion order is guaranteed: Since Python 3.7,
dict.values()returns values in the order their keys were inserted. This is part of the language spec, not an implementation detail. - Know the limitations: No indexing, no set operations, and no mutating the dictionary's size during iteration. Convert to a list, set, or tuple when you need those capabilities.
- Use views directly when possible: For iteration, aggregation, and membership testing, the view is sufficient and more efficient than creating a copy.
The next time you write for v in d.values(), you will know exactly what is happening beneath the surface: a lightweight proxy, zero memory overhead, guaranteed insertion order, and a design shaped by the collective wisdom of the Python community's most influential contributors.
Sources and References
Python Enhancement Proposals (PEPs):
- PEP 3106 — "Revamping dict.keys(), .values() and .items()" by Guido van Rossum (2006)
- PEP 468 — "Preserving the order of **kwargs in a function" by Eric Snow (2014)
- PEP 509 — "Add a private version to dict" by Victor Stinner (2016)
- PEP 584 — "Add Union Operators To dict" by Brandt Bucher and Guido van Rossum (2019)
- PEP 699 — "Remove private dict version field added in PEP 509" by Ken Jin (2022)
CPython development and implementation:
- Raymond Hettinger, "More compact dictionaries with faster iteration", python-dev mailing list (December 2012)
- Maciej Fijałkowski and Armin Rigo, "Faster, more memory efficient and more ordered dictionaries on PyPy", PyPy Blog (January 2015)
- INADA Naoki, Implement compact dict, CPython commit for Issue #27350 (September 2016)
- Victor Stinner, "Python 3.6 dict becomes compact and gets a private version", python-dev mailing list (September 2016)
- Guido van Rossum, "Dict keeps insertion order" ruling, python-dev mailing list (December 2017)
Official Python documentation: