This tutorial teaches how to work with the Python data structure collections.deque
and provides 7 example use cases.
deque
tutorialIf you already know how to manipulate a deque
, feel free to skip to the example use cases!
deque
?A deque
in Python is a data structure from the module collections
.
A deque
is often compared to a Python list because they are both ordered containers that let you append and pop elements from the right efficiently.
The code below shows the similarities:
from collections import deque
my_list = []
my_deque = deque() # Create a `deque`.
my_list.append(1)
my_deque.append(1) # Append an element to its end.
my_list.extend(range(2, 5))
my_deque.extend(range(2, 5)) # Extend the `deque` with more elements.
popped_from_list = my_list.pop()
popped_from_deque = my_deque.pop() # Pop an element from the end.
print(my_list, popped_from_list) # [1, 2, 3] 4
print(my_deque, popped_from_deque) # deque([1, 2, 3]) 4
There are two main differences between deque
s and lists:
deque
(on a list, it becomes slower as the list grows); anddeque
with its parameter maxlen
.These two differences are the ones that play a key role when determining whether you should use a deque
, a list, or any other container.
deque
As I've shown above, an empty deque
can be created by simply typing deque()
:
from collections import deque
my_deque = deque()
print(my_deque) # deque([])
A deque
can also be seeded with elements from any other iterable:
from collections import deque
deque_with_chars = deque("hello!")
print(deque_with_chars) # deque(['h', 'e', 'l', 'l', 'o', '!'])
deque_with_ints = deque(range(5))
print(deque_with_ints) # deque([0, 1, 2, 3, 4])
deque_with_things = deque([True, None, {}, set()])
print(deque_with_things) # deque([True, None, {}, set()])
When you instantiate a deque
, you can also specify the parameter maxlen
.
The deque
parameter maxlen
will restrict the maximum length that your deque
can have.
For example, the deque
below has a maximum length of 2
, which means that adding more than two elements will result in other elements being pushed off of the deque
:
from collections import deque
deque_size_2 = deque([1, 2], maxlen=2)
print(deque_size_2) # deque([1, 2])
deque_size_2.append(3) # This forces an element to pop from the left.
# Now, we'll see that the `1` is gone:
print(deque_size_2) # deque([2, 3])
The parameter maxlen
is what allows you to use Python's deque
for a number of interesting examples that I'll show below.
deque
The deque
documentation has a comprehensive list of all the methods that deque
s support but I'll show you the most common ones here.
deque
To add elements to a deque
, you'll typically do it in one of three ways:
deque
with some initial elements;deque
; ordeque
with another iterable.I've already shown you how to initialise your deque
with some elements.
To append elements, you can use the method append
to add elements on the right and you can use the method appendleft
to add elements on the left:
from collections import deque
my_deque = deque()
my_deque.append(1)
my_deque.append(2)
print(my_deque) # deque([1, 2])
my_deque.appendleft(0)
my_deque.appendleft(-1)
print(my_deque) # deque([-1, 0, 1, 2])
If you have another iterable it
and you want to add all of the elements inside it
to a deque
, you can use the methods extend
and extendleft
:
from collections import deque
my_deque = deque()
# Do this:
my_deque.extend(range(5))
print(my_deque) # deque([0, 1, 2, 3, 4])
another_deque = deque()
another_deque.extendleft(range(5))
print(another_deque) # deque([4, 3, 2, 1, 0])
Note that when you use the method extendleft
, the elements in the deque
will be in reversed order when compared to the original iterable.
You can also insert an element in the middle of a deque
with the method insert
, but that is an inefficient operation on a deque
(just like on a list).
deque
To remove elements from a deque
, you'll probably want to use the methods
pop
– to pop an element from the right;popleft
– to pop an element... from the left; andclear
– to clear all of the elements in the deque
.Here is an example showing all three methods:
from collections import deque
my_deque = deque(range(10))
print(my_deque) # deque([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
my_deque.pop() # Pops the 9.
my_deque.pop() # Pops the 8.
print(my_deque) # deque([0, 1, 2, 3, 4, 5, 6, 7])
my_deque.popleft() # Pops the 0.
my_deque.popleft() # Pops the 1.
print(my_deque) # deque([2, 3, 4, 5, 6, 7])
my_deque.clear() # Clear everything.
print(my_deque) # deque([])
deque
methodsA deque
provides some other useful methods (again, check the docs!).
The list below gives you a quick overview of these methods:
copy
– creates a shallow copy of the deque
;count
– counts how many times an element appears;index
– finds the first position where a given value occurs;remove
– removes the first occurrence of a value;reverse
– reverses the deque
in-place; androtate
– rotates the elements inside the deque
.The method rotate
is the only method that lists don't support, so I'll show you a brief example:
from collections import deque
my_deque = deque(range(5))
print(my_deque) # deque([0, 1, 2, 3, 4])
my_deque.rotate() # Same as `.rotate(1)`.
print(my_deque) # deque([4, 0, 1, 2, 3])
my_deque.rotate(2)
print(my_deque) # deque([2, 3, 4, 0, 1])
my_deque.rotate(-3) # Undo the previous two rotations.
print(my_deque) # deque([0, 1, 2, 3, 4])
When the argument is positive, rotate(n)
means “push the elements of the deque
a total of n
positions forward”, which has the consequence of bringing the last n
items of the deque
to the beginning.
A negative argument does the opposite.
deque
examplesThe characteristics of a deque
let you use it for a variety of interesting things.
These will typically make use of the fact that a deque
can be easily modified on both ends and/or that a deque
lets you cap its size with the parameter maxlen
.
In this section we'll go through some examples of use cases for the deque
data structure that highlight its capabilities:
itertools.pairwise
;These are just some examples.
If you know of a good use case of deque
feel free to comment below and I might add it here.
Given the fact that deque
s have the parameter maxlen
, they are particularly suitable for when you want to access the last elements of iterables that you can't slice.
When you want the last 5 items of a list or the last 5 characters of a string, you'd use a negative slice:
my_list = [10, 20, 30, 40, 50, 60, True, False, None, 1, 2]
print(my_list[-5:]) # [True, False, None, 1, 2]
my_string = "mathspp.com"
print(my_string[-5:]) # p.com
However, there are iterables that do not support slicing.
Files, for example, cannot be sliced!
So, if you want the last five lines of a file, your best bet is using a deque
:
from collections import deque
with open(filepath, "r") as file:
last_5_lines = deque(file, maxlen=5)
# last_5_lines contains the last 5 lines of the file.
print(last_5_lines)
This idiom traverses the whole file.
If efficiency is critical, consider taking a look at the module
mmap
from the standard library.
The pattern of using a deque
to get the last few items of an iterable can take many useful forms.
The next section shows a “completely different use case” for a deque
with a fixed maximum size that ends up being exactly the same thing, except in a different context.
If you set a deque
to have a maximum size of 1
, you can use it to fetch the last element of any iterable:
from collections import deque
my_string = "Python"
print(deque(my_string, maxlen=1)) # deque(['n'])
my_numbers = range(100)
print(deque(my_numbers, maxlen=1)) # deque([99])
my_list = [42, False, dict(), ..., True]
print(deque(my_list, maxlen=1)) # deque([True])
If you combine this with a generator expression and a predicate function, you have an idiom that is very space efficient and that finds the last element of an iterable that satisfies the given predicate.
As a small example, the snippet below shows how to find the last vowel in a sentence:
from collections import deque
sentence = "The quick brown fox jumps over the last dog!"
# The last vowel is an 'o' --------------------------^
vowels = set("aeiouAEIOU")
last_vowel = deque((char for char in sentence if char in vowels), maxlen=1)
try:
print(last_vowel.pop()) # o
except IndexError:
print("No vowels found.")
The fact that you are using a generator expression and a deque
that can only hold one element at a time means that you are being as space efficient as possible and this space efficiency can be quite handy if you're going over the lines of a huge file in disk, for example.
You can even wrap this functionality in a class that looks like a function:
from collections import deque
_SENTINEL = object()
class last:
def __init__(self, iterable):
self.maybe_value = deque(iterable, maxlen=1)
def item(self, *, default=_SENTINEL):
try:
return self.maybe_value.pop()
except IndexError:
if default is _SENTINEL:
raise ValueError("No last element.") from None
else:
return default
This allows you to use last
as a function that accepts an iterable and finds the last value of that iterable.
Then, you use the method item
to materialise the item.
If the iterable was empty, then item
will either raise an ValueError
or return the default value you provide as a keyword argument.
Here are some example applications:
print(last("Rodrigo").item()) # o
print(last(range(10)).item()) # 9
print(last([]).item(default=42)) # 42
print(last([]).item()) # ValueError
Another clever usage of the parameter maxlen
of a deque
lets you create an idiom to exhaust any iterator.
If it
is an iterator, then deque(it, maxlen=0)
will exhaust the iterator fully.
This is example is more exotic than the other two but there are cases where you want to reach the end of an iterator without necessarily wanting to do anything with the items you find along the way.
To show that deque(..., maxlen=0)
does exhaust an iterator, the snippet below uses a generator expression whose last item is given by the expression 1 / 0
, which will raise a ZeroDivisionError
when that item is computed.
Creating the generator expression isn't enough to trigger the error:
gen_expr = (1 / num for num in range(10, -1, -1))
You can also advance the generator a couple of steps and it won't trigger the error:
print(next(gen_expr)) # 0.1
print(next(gen_expr)) # 0.1111111111111111
print(next(gen_expr)) # 0.125
But it's only when you exhaust the generator with the help of deque
that you get the error:
from collections import deque
gen_expr = (1 / num for num in range(10, -1, -1))
next(gen_expr)
next(gen_expr)
next(gen_expr)
deque(gen_expr, maxlen=0) # ZeroDivisionError
This idiom can be helpful when you want to make sure a given iterable can be exhausted without erroring out but at the same time you're not interested in the contents of the iterable.
You can also wrap this idiom in a short function:
from collections import deque
def exhaust(iterable):
deque(iterable, maxlen=0)
A moving average is an average computed over an iterable where you only consider a subset of all of the values you have.
For example, the average of the list [0, 1, 2, 3, 4]
is 2.
However, if you compute the moving average over that list with a window of size 3, you get a succession of increasing values:
[0, 1, 2, 3, 4]
^^^^^^^ average of 1
^^^^^^^ average of 2
^^^^^^^ average of 3
Moving averages have many applications in the world of finance, for example.
At the time of writing, the qutebrowser project (a browser written in Python) uses a deque
to compute a moving average to estimate how much time a download will take to complete.
To compute a moving average with a deque
, we make use of the parameter maxlen
once more, which we set to be the size of the window in the moving average.
This means that, when the deque
is full, appending a new element to the deque
creates the next window whose average we can compute.
The snippet below implements a function moving_averages
that accepts an iterable and computes all of the moving averages of a given size on that iterable:
from collections import deque
def moving_averages(data, window_size):
values = []
window = deque(maxlen=window_size)
for element in data:
window.append(element)
values.append(sum(window) / len(window))
return values
Applying this to the previous example list [0, 1, 2, 3, 4]
shows that our implementation computes some initial values that didn't include a full window:
averages = moving_averages([0, 1, 2, 3, 4], 3)
print(averages) # [0.0, 0.5, 1.0, 2.0, 3.0]
Depending on the context, these “incomplete” averages may or may not be relevant.
If the first values are not relevant, you can use itertools.islice
to fill the window before starting to compute averages:
from collections import deque
from itertools import islice
def moving_averages(data, window_size):
data = iter(data)
values = []
window = deque(islice(data, window_size-1), maxlen=window_size)
for element in data:
window.append(element)
values.append(sum(window) / len(window))
return values
averages = moving_averages([0, 1, 2, 3, 4], 3)
print(averages) # [1.0, 2.0, 3.0]
itertools.pairwise
Speaking of itertools
, a deque
is a simple way of implementing itertools.pairwise
, which is only available in Python 3.10 or later.
pairwise
accepts an iterable and produces the (overlapping) pairs of consecutive elements:
from itertools import pairwise
my_list = [42, 73, 16, 0, 10]
for a, b in pairwise(my_list):
print(a, b)
"""
Output:
42 73
73 16
16 0
0 10
"""
If you don't have access to pairwise
, or if you need to implement a more general version of pairwise
, you can adapt the implement of moving_averages
above.
Instead of summing the values inside the “window” and then dividing by the length of the “window”, you return a tuple with the elements:
from collections import deque
from itertools import islice
def pairwise_(data):
data = iter(data)
window = deque(islice(data, 1), maxlen=2)
for value in data:
window.append(value)
yield tuple(window)
This produces the same result if we apply our implementation of pairwise_
to our previous list:
my_list = [42, 73, 16, 0, 10]
for a, b in pairwise_(my_list):
print(a, b)
"""
Output:
42 73
73 16
16 0
0 10
"""
If we need a more general version of pairwise
, to produce tuples of length n
, we can adapt the previous implementation:
from collections import deque
from itertools import islice
def n_tuples(data, n):
"""Produces consecutive overlapping tuples of size `n`."""
data = iter(data)
window = deque(islice(data, n - 1), maxlen=n)
for value in data:
window.append(value)
yield tuple(window)
This works in the same way:
my_list = [42, 73, 16, 0, 10]
for a, b, c in n_tuples(my_list, 3):
print(a, b, c)
"""
Output:
42 73 16
73 16 0
16 0 10
"""
Notice how this is remarkably similar to the function moving_averages
from the section on computing moving averages with deque
.
Textual, a framework that lets you create TUIs in Python (and the framework I work on, at the time of writing), uses a deque
to create the stack of undo/redo actions that are allowed in its text area.
Most (if not all!) text editors let you undo your recent changes, in case you want to go back to a previous version of what you had written. For this to be possible, the text editor keeps a stack of your changes. Each time you press “undo”, the editor pops an item off that stack and undoes that change.
Theoretically, this stack of changes could be infinite and you could undo all of the changes you ever made on a given document. In practice, the stack size is capped. That's because you don't want your computer to grind to a halt because your text editor is using up all of the RAM to keep a huge stack of changes you might want to undo later.
In Python, a list is an excellent data structure for a stack because of its methods .pop
and .append
.
If you want the size of the stack to be capped, then a deque
is the natural candidate for that.
Appending on and popping from the right preserves the stack semantics and the parameter maxlen
makes sure your stack doesn't grow too much.
By using a deque
with a maximum size specified by maxlen
, the undo/redo stack can be used without having to worry about its size:
Making sure the stack doesn't go over a certain size is managed automatically by the deque
.
I've also found that a deque
can be quite helpful when a method is producing intermediate results and it needs to buffer them.
For example, in my “Building a Python Compiler and Interpreter” blog series, I implement a tokenizer for Python source code.
This tokenizer has a method next_token
that computes and emits the next token:
class Tokenizer:
# ...
def next_token(self) -> Token:
# ...
The tokenizer has a token buffer implemented with a deque
.
If the buffer has any tokens when Tokenizer.next_token
is called, we pop a token from the left of the buffer instead of computing the next one:
from collections import deque
class Tokenizer:
def __init__(self, code: str) -> None:
# ...
self.next_tokens: deque[Token] = deque()
def next_token(self) -> Token:
if self.next_tokens:
return self.next_tokens.popleft()
# ...
This is helpful because there are certain moments when the tokenizer is trying to produce only one token but ends up producing many tokens at the same time.
When that is the case, we return only one token and we save the other tokens in the buffer.
You can see this, in context, in the “Building a Python Compiler and Interpreter” article where we add the if
statement.
+35 chapters. +400 pages. Hundreds of examples. Over 30,000 readers!
My book “Pydon'ts” teaches you how to write elegant, expressive, and Pythonic code, to help you become a better developer. >>> Download it here 🐍🚀.
collections.deque
, https://docs.python.org/3/library/collections.html#collections.deque [last accessed 18-01-2024];collections.deque
for?” Twitter post, https://twitter.com/mathsppblog/status/1747904120130855312 [last accessed 20-01-2024];