PythonMastery
reference 5 min read · lesson 14 of 25 in Reference

Python itertools Cheat Sheet

1 · The lesson

read

The itertools module is a set of small tools for looping: counting, joining, slicing, grouping and combining. Every tool here is lazy: it makes one item at a time, only when the loop asks for the next item, and the tool is used up after one pass. Because of this, the examples wrap each tool in list() to show all its items. For the full tutorial, see Itertools and Generators.

An iterable is anything you can loop over: a list, a string, a range, a file. An iterator is the object that gives the items one by one. next() asks it for the next item.

python
from itertools import (accumulate, batched, chain, combinations,
                       combinations_with_replacement, compress, count, cycle,
                       dropwhile, filterfalse, groupby, islice, pairwise,
                       permutations, product, repeat, starmap, takewhile, tee,
                       zip_longest)

1. Endless: always add a stop

count(), cycle() and repeat() never stop on their own. Take a fixed number of items with islice(), pair them with a finite input in zip(), or break out of the loop.

python
print(list(islice(count(10, 2), 3)))      # [10, 12, 14]
print(list(islice(cycle("ab"), 5)))       # ['a', 'b', 'a', 'b', 'a']
print(list(repeat(0, 3)))                 # [0, 0, 0]
print(list(zip(count(1), "ab")))          # [(1, 'a'), (2, 'b')]
+ setup added so this can run · re-runs 1 earlier block on this page first
# Lightweight mock for objects whose attributes/methods aren't critical
class _AutoMock:
    def __init__(self, name='mock'): self._name = name
    def __getattr__(self, k): return _AutoMock(self._name + '.' + k)
    def __call__(self, *a, **kw):
        print('-> ' + self._name + '() called')
        return _AutoMock(self._name + '()')
    def __repr__(self): return '<mock ' + self._name + '>'
    def __str__(self): return '<mock ' + self._name + '>'
    def __bool__(self): return True
    def __iter__(self): return iter([])
    def __len__(self): return 0
    def __getitem__(self, k): return _AutoMock(self._name + '[...]')
    def __setitem__(self, k, v): pass
    def __enter__(self): return self
    def __exit__(self, *a): return False
    async def __aenter__(self): return self
    async def __aexit__(self, *a): return False
    def __add__(self, o): return self
    def __radd__(self, o): return self
    def __sub__(self, o): return self
    def __mul__(self, o): return self
    def __rmul__(self, o): return self
    def __truediv__(self, o): return self
    def __eq__(self, o): return isinstance(o, _AutoMock)
    def __hash__(self): return hash(self._name)
    def __lt__(self, o): return True
    def __le__(self, o): return True
    def __gt__(self, o): return False
    def __ge__(self, o): return False
    def __mro_entries__(self, bases): return (object,)

def islice(*_a, **_kw):
    print('-> islice() called')
    return _AutoMock('islice()')
def repeat(*_a, **_kw):
    print('-> repeat() called')
    return _AutoMock('repeat()')
def count(*_a, **_kw):
    print('-> count() called')
    return _AutoMock('count()')
def cycle(*_a, **_kw):
    print('-> cycle() called')
    return _AutoMock('cycle()')

# Earlier blocks on this page that this one builds on, re-run quietly first.
try:
    from itertools import accumulate, batched, chain, combinations, combinations_with_replacement, compress, count, cycle, dropwhile, filterfalse, groupby, islice, pairwise, permutations, product, repeat, starmap, takewhile, tee, zip_longest
except Exception:
    pass
_pm_earlier = [
    'from itertools import (accumulate, batched, chain, combinations,\n                       combinations_with_replacement, compress, count, cycle,\n                       dropwhile, filterfalse, groupby, islice, pairwise,\n                       permutations, product, repeat, starmap, takewhile, tee,\n                       zip_longest)',
]
def _pm_run_earlier(_srcs, _g):
    import sys as _s, io as _io, warnings as _w
    for _src in _srcs:
        _o, _e = _s.stdout, _s.stderr
        _s.stdout = _s.stderr = _io.StringIO()
        try:
            with _w.catch_warnings():
                _w.simplefilter('ignore')
                exec(compile(_src, '<earlier block>', 'exec'), _g)
        except (Exception, SystemExit):
            pass
        finally:
            _s.stdout, _s.stderr = _o, _e
    if 'matplotlib.pyplot' in _s.modules:
        _s.modules['matplotlib.pyplot'].close('all')   # their plots belong to their own blocks
_pm_run_earlier(_pm_earlier, globals())
del _pm_earlier, _pm_run_earlier

count(start, step) counts up forever. cycle() repeats its input forever. repeat() repeats one value, forever unless you give a count. zip() stops at the shortest input, so count(1) stops after two items here.

2. Slice and stop

python
print(list(islice("abcdef", 3)))                 # ['a', 'b', 'c']
print(list(islice("abcdef", 2, None, 2)))        # ['c', 'e']
print(next(islice("abc", 1, None)))              # b
print(list(takewhile(str.isdigit, "12ab3")))     # ['1', '2']
print(list(dropwhile(str.isdigit, "12ab3")))     # ['a', 'b', '3']
print(list(compress("abcd", [1, 0, 1, 0])))      # ['a', 'c']
print(list(filterfalse(str.isdigit, "a1b2")))    # ['a', 'b']
+ setup added so this can run · re-runs 1 earlier block on this page first
# Lightweight mock for objects whose attributes/methods aren't critical
class _AutoMock:
    def __init__(self, name='mock'): self._name = name
    def __getattr__(self, k): return _AutoMock(self._name + '.' + k)
    def __call__(self, *a, **kw):
        print('-> ' + self._name + '() called')
        return _AutoMock(self._name + '()')
    def __repr__(self): return '<mock ' + self._name + '>'
    def __str__(self): return '<mock ' + self._name + '>'
    def __bool__(self): return True
    def __iter__(self): return iter([])
    def __len__(self): return 0
    def __getitem__(self, k): return _AutoMock(self._name + '[...]')
    def __setitem__(self, k, v): pass
    def __enter__(self): return self
    def __exit__(self, *a): return False
    async def __aenter__(self): return self
    async def __aexit__(self, *a): return False
    def __add__(self, o): return self
    def __radd__(self, o): return self
    def __sub__(self, o): return self
    def __mul__(self, o): return self
    def __rmul__(self, o): return self
    def __truediv__(self, o): return self
    def __eq__(self, o): return isinstance(o, _AutoMock)
    def __hash__(self): return hash(self._name)
    def __lt__(self, o): return True
    def __le__(self, o): return True
    def __gt__(self, o): return False
    def __ge__(self, o): return False
    def __mro_entries__(self, bases): return (object,)

def islice(*_a, **_kw):
    print('-> islice() called')
    return _AutoMock('islice()')
def takewhile(*_a, **_kw):
    print('-> takewhile() called')
    return _AutoMock('takewhile()')
def dropwhile(*_a, **_kw):
    print('-> dropwhile() called')
    return _AutoMock('dropwhile()')
def compress(*_a, **_kw):
    print('-> compress() called')
    return _AutoMock('compress()')
def filterfalse(*_a, **_kw):
    print('-> filterfalse() called')
    return _AutoMock('filterfalse()')

# Earlier blocks on this page that this one builds on, re-run quietly first.
try:
    from itertools import accumulate, batched, chain, combinations, combinations_with_replacement, compress, count, cycle, dropwhile, filterfalse, groupby, islice, pairwise, permutations, product, repeat, starmap, takewhile, tee, zip_longest
except Exception:
    pass
_pm_earlier = [
    'from itertools import (accumulate, batched, chain, combinations,\n                       combinations_with_replacement, compress, count, cycle,\n                       dropwhile, filterfalse, groupby, islice, pairwise,\n                       permutations, product, repeat, starmap, takewhile, tee,\n                       zip_longest)',
]
def _pm_run_earlier(_srcs, _g):
    import sys as _s, io as _io, warnings as _w
    for _src in _srcs:
        _o, _e = _s.stdout, _s.stderr
        _s.stdout = _s.stderr = _io.StringIO()
        try:
            with _w.catch_warnings():
                _w.simplefilter('ignore')
                exec(compile(_src, '<earlier block>', 'exec'), _g)
        except (Exception, SystemExit):
            pass
        finally:
            _s.stdout, _s.stderr = _o, _e
    if 'matplotlib.pyplot' in _s.modules:
        _s.modules['matplotlib.pyplot'].close('all')   # their plots belong to their own blocks
_pm_run_earlier(_pm_earlier, globals())
del _pm_earlier, _pm_run_earlier
ToolWhat it does
islice(it, stop) or islice(it, start, stop, step)Slices anything you can loop over, like [start:stop:step]. Negative numbers are not allowed.
takewhile(test, it)Gives items while the test is true, and stops at the first false one
dropwhile(test, it)Skips items while the test is true, then gives all the rest
compress(data, flags)Keeps the items whose flag in the second list is true
filterfalse(test, it)The opposite of filter(): keeps the items where the test is false

3. Join and flatten

python
print(list(chain("ab", "cd")))                        # ['a', 'b', 'c', 'd']
print(list(chain.from_iterable([[1, 2], [3]])))       # [1, 2, 3]
print(list(zip_longest("ab", "xyz", fillvalue="-")))  # [('a', 'x'), ('b', 'y'), ('-', 'z')]
+ setup added so this can run · re-runs 1 earlier block on this page first
# Lightweight mock for objects whose attributes/methods aren't critical
class _AutoMock:
    def __init__(self, name='mock'): self._name = name
    def __getattr__(self, k): return _AutoMock(self._name + '.' + k)
    def __call__(self, *a, **kw):
        print('-> ' + self._name + '() called')
        return _AutoMock(self._name + '()')
    def __repr__(self): return '<mock ' + self._name + '>'
    def __str__(self): return '<mock ' + self._name + '>'
    def __bool__(self): return True
    def __iter__(self): return iter([])
    def __len__(self): return 0
    def __getitem__(self, k): return _AutoMock(self._name + '[...]')
    def __setitem__(self, k, v): pass
    def __enter__(self): return self
    def __exit__(self, *a): return False
    async def __aenter__(self): return self
    async def __aexit__(self, *a): return False
    def __add__(self, o): return self
    def __radd__(self, o): return self
    def __sub__(self, o): return self
    def __mul__(self, o): return self
    def __rmul__(self, o): return self
    def __truediv__(self, o): return self
    def __eq__(self, o): return isinstance(o, _AutoMock)
    def __hash__(self): return hash(self._name)
    def __lt__(self, o): return True
    def __le__(self, o): return True
    def __gt__(self, o): return False
    def __ge__(self, o): return False
    def __mro_entries__(self, bases): return (object,)

chain = _AutoMock('chain')
def zip_longest(*_a, **_kw):
    print('-> zip_longest() called')
    return _AutoMock('zip_longest()')

# Earlier blocks on this page that this one builds on, re-run quietly first.
try:
    from itertools import accumulate, batched, chain, combinations, combinations_with_replacement, compress, count, cycle, dropwhile, filterfalse, groupby, islice, pairwise, permutations, product, repeat, starmap, takewhile, tee, zip_longest
except Exception:
    pass
_pm_earlier = [
    'from itertools import (accumulate, batched, chain, combinations,\n                       combinations_with_replacement, compress, count, cycle,\n                       dropwhile, filterfalse, groupby, islice, pairwise,\n                       permutations, product, repeat, starmap, takewhile, tee,\n                       zip_longest)',
]
def _pm_run_earlier(_srcs, _g):
    import sys as _s, io as _io, warnings as _w
    for _src in _srcs:
        _o, _e = _s.stdout, _s.stderr
        _s.stdout = _s.stderr = _io.StringIO()
        try:
            with _w.catch_warnings():
                _w.simplefilter('ignore')
                exec(compile(_src, '<earlier block>', 'exec'), _g)
        except (Exception, SystemExit):
            pass
        finally:
            _s.stdout, _s.stderr = _o, _e
    if 'matplotlib.pyplot' in _s.modules:
        _s.modules['matplotlib.pyplot'].close('all')   # their plots belong to their own blocks
_pm_run_earlier(_pm_earlier, globals())
del _pm_earlier, _pm_run_earlier

chain() goes through several inputs one after another, without copying them into a new list. chain.from_iterable() does the same for a list of lists, which flattens it by one level. zip_longest() keeps going until the longest input ends and fills the gaps with fillvalue.

Plain zip() stops quietly at the shortest input. Pass strict=True to get an error when the lengths differ:

python
try:
    list(zip("ab", "xyz", strict=True))
except ValueError as e:
    print("ValueError:", e)
# ValueError: zip() argument 2 is longer than argument 1

4. Pairs and chunks

python
print(list(pairwise([1, 4, 9])))                  # [(1, 4), (4, 9)]
print([b - a for a, b in pairwise([1, 4, 9])])    # [3, 5]
print(list(batched("abcde", 2)))                  # [('a', 'b'), ('c', 'd'), ('e',)]
+ setup added so this can run · re-runs 1 earlier block on this page first
# Lightweight mock for objects whose attributes/methods aren't critical
class _AutoMock:
    def __init__(self, name='mock'): self._name = name
    def __getattr__(self, k): return _AutoMock(self._name + '.' + k)
    def __call__(self, *a, **kw):
        print('-> ' + self._name + '() called')
        return _AutoMock(self._name + '()')
    def __repr__(self): return '<mock ' + self._name + '>'
    def __str__(self): return '<mock ' + self._name + '>'
    def __bool__(self): return True
    def __iter__(self): return iter([])
    def __len__(self): return 0
    def __getitem__(self, k): return _AutoMock(self._name + '[...]')
    def __setitem__(self, k, v): pass
    def __enter__(self): return self
    def __exit__(self, *a): return False
    async def __aenter__(self): return self
    async def __aexit__(self, *a): return False
    def __add__(self, o): return self
    def __radd__(self, o): return self
    def __sub__(self, o): return self
    def __mul__(self, o): return self
    def __rmul__(self, o): return self
    def __truediv__(self, o): return self
    def __eq__(self, o): return isinstance(o, _AutoMock)
    def __hash__(self): return hash(self._name)
    def __lt__(self, o): return True
    def __le__(self, o): return True
    def __gt__(self, o): return False
    def __ge__(self, o): return False
    def __mro_entries__(self, bases): return (object,)

def pairwise(*_a, **_kw):
    print('-> pairwise() called')
    return _AutoMock('pairwise()')
def batched(*_a, **_kw):
    print('-> batched() called')
    return _AutoMock('batched()')

# Earlier blocks on this page that this one builds on, re-run quietly first.
try:
    from itertools import accumulate, batched, chain, combinations, combinations_with_replacement, compress, count, cycle, dropwhile, filterfalse, groupby, islice, pairwise, permutations, product, repeat, starmap, takewhile, tee, zip_longest
except Exception:
    pass
_pm_earlier = [
    'from itertools import (accumulate, batched, chain, combinations,\n                       combinations_with_replacement, compress, count, cycle,\n                       dropwhile, filterfalse, groupby, islice, pairwise,\n                       permutations, product, repeat, starmap, takewhile, tee,\n                       zip_longest)',
]
def _pm_run_earlier(_srcs, _g):
    import sys as _s, io as _io, warnings as _w
    for _src in _srcs:
        _o, _e = _s.stdout, _s.stderr
        _s.stdout = _s.stderr = _io.StringIO()
        try:
            with _w.catch_warnings():
                _w.simplefilter('ignore')
                exec(compile(_src, '<earlier block>', 'exec'), _g)
        except (Exception, SystemExit):
            pass
        finally:
            _s.stdout, _s.stderr = _o, _e
    if 'matplotlib.pyplot' in _s.modules:
        _s.modules['matplotlib.pyplot'].close('all')   # their plots belong to their own blocks
_pm_run_earlier(_pm_earlier, globals())
del _pm_earlier, _pm_run_earlier

pairwise() gives each item with the next one, so the pairs overlap. pairwise() is useful for finding the differences between neighbouring items. batched() (Python 3.12+) cuts the input into chunks of a fixed size. The last chunk can be shorter. In Python 3.13+, batched(..., strict=True) raises ValueError if the last chunk is short.

5. Combinations

python
print(list(product("ab", "xy")))      # [('a', 'x'), ('a', 'y'), ('b', 'x'), ('b', 'y')]
print(list(product([0, 1], repeat=2)))  # [(0, 0), (0, 1), (1, 0), (1, 1)]
print(len(list(permutations("abc", 2))))  # 6
print(list(combinations("abc", 2)))   # [('a', 'b'), ('a', 'c'), ('b', 'c')]
print(list(combinations_with_replacement("ab", 2)))  # [('a', 'a'), ('a', 'b'), ('b', 'b')]
+ setup added so this can run · re-runs 1 earlier block on this page first
# Lightweight mock for objects whose attributes/methods aren't critical
class _AutoMock:
    def __init__(self, name='mock'): self._name = name
    def __getattr__(self, k): return _AutoMock(self._name + '.' + k)
    def __call__(self, *a, **kw):
        print('-> ' + self._name + '() called')
        return _AutoMock(self._name + '()')
    def __repr__(self): return '<mock ' + self._name + '>'
    def __str__(self): return '<mock ' + self._name + '>'
    def __bool__(self): return True
    def __iter__(self): return iter([])
    def __len__(self): return 0
    def __getitem__(self, k): return _AutoMock(self._name + '[...]')
    def __setitem__(self, k, v): pass
    def __enter__(self): return self
    def __exit__(self, *a): return False
    async def __aenter__(self): return self
    async def __aexit__(self, *a): return False
    def __add__(self, o): return self
    def __radd__(self, o): return self
    def __sub__(self, o): return self
    def __mul__(self, o): return self
    def __rmul__(self, o): return self
    def __truediv__(self, o): return self
    def __eq__(self, o): return isinstance(o, _AutoMock)
    def __hash__(self): return hash(self._name)
    def __lt__(self, o): return True
    def __le__(self, o): return True
    def __gt__(self, o): return False
    def __ge__(self, o): return False
    def __mro_entries__(self, bases): return (object,)

def product(*_a, **_kw):
    print('-> product() called')
    return _AutoMock('product()')
def combinations(*_a, **_kw):
    print('-> combinations() called')
    return _AutoMock('combinations()')
def combinations_with_replacement(*_a, **_kw):
    print('-> combinations_with_replacement() called')
    return _AutoMock('combinations_with_replacement()')
def permutations(*_a, **_kw):
    print('-> permutations() called')
    return _AutoMock('permutations()')

# Earlier blocks on this page that this one builds on, re-run quietly first.
try:
    from itertools import accumulate, batched, chain, combinations, combinations_with_replacement, compress, count, cycle, dropwhile, filterfalse, groupby, islice, pairwise, permutations, product, repeat, starmap, takewhile, tee, zip_longest
except Exception:
    pass
_pm_earlier = [
    'from itertools import (accumulate, batched, chain, combinations,\n                       combinations_with_replacement, compress, count, cycle,\n                       dropwhile, filterfalse, groupby, islice, pairwise,\n                       permutations, product, repeat, starmap, takewhile, tee,\n                       zip_longest)',
]
def _pm_run_earlier(_srcs, _g):
    import sys as _s, io as _io, warnings as _w
    for _src in _srcs:
        _o, _e = _s.stdout, _s.stderr
        _s.stdout = _s.stderr = _io.StringIO()
        try:
            with _w.catch_warnings():
                _w.simplefilter('ignore')
                exec(compile(_src, '<earlier block>', 'exec'), _g)
        except (Exception, SystemExit):
            pass
        finally:
            _s.stdout, _s.stderr = _o, _e
    if 'matplotlib.pyplot' in _s.modules:
        _s.modules['matplotlib.pyplot'].close('all')   # their plots belong to their own blocks
_pm_run_earlier(_pm_earlier, globals())
del _pm_earlier, _pm_run_earlier
ToolGives you
product(a, b)Every pairing of an item from a with an item from b, the same as nested for loops
product(x, repeat=2)Every pairing of x with itself. With [0, 1], every on/off pattern
permutations(x, 2)Every ordered pick of 2: ('a', 'b') and ('b', 'a') both count
combinations(x, 2)Every pick of 2 where order does not matter: no ('b', 'a')
combinations_with_replacement(x, 2)Like combinations, but an item can be picked again

6. Running totals

accumulate() gives the result so far after each item.

python
from operator import mul

print(list(accumulate([1, 2, 3, 4])))             # [1, 3, 6, 10]
print(list(accumulate([3, 1, 5, 2], max)))        # [3, 3, 5, 5]
print(list(accumulate([1, 2, 3], mul)))           # [1, 2, 6]
print(list(accumulate([5, -2], initial=100)))     # [100, 105, 103]
+ setup added so this can run · re-runs 1 earlier block on this page first
# Lightweight mock for objects whose attributes/methods aren't critical
class _AutoMock:
    def __init__(self, name='mock'): self._name = name
    def __getattr__(self, k): return _AutoMock(self._name + '.' + k)
    def __call__(self, *a, **kw):
        print('-> ' + self._name + '() called')
        return _AutoMock(self._name + '()')
    def __repr__(self): return '<mock ' + self._name + '>'
    def __str__(self): return '<mock ' + self._name + '>'
    def __bool__(self): return True
    def __iter__(self): return iter([])
    def __len__(self): return 0
    def __getitem__(self, k): return _AutoMock(self._name + '[...]')
    def __setitem__(self, k, v): pass
    def __enter__(self): return self
    def __exit__(self, *a): return False
    async def __aenter__(self): return self
    async def __aexit__(self, *a): return False
    def __add__(self, o): return self
    def __radd__(self, o): return self
    def __sub__(self, o): return self
    def __mul__(self, o): return self
    def __rmul__(self, o): return self
    def __truediv__(self, o): return self
    def __eq__(self, o): return isinstance(o, _AutoMock)
    def __hash__(self): return hash(self._name)
    def __lt__(self, o): return True
    def __le__(self, o): return True
    def __gt__(self, o): return False
    def __ge__(self, o): return False
    def __mro_entries__(self, bases): return (object,)

def accumulate(*_a, **_kw):
    print('-> accumulate() called')
    return _AutoMock('accumulate()')

# Earlier blocks on this page that this one builds on, re-run quietly first.
try:
    from itertools import accumulate, batched, chain, combinations, combinations_with_replacement, compress, count, cycle, dropwhile, filterfalse, groupby, islice, pairwise, permutations, product, repeat, starmap, takewhile, tee, zip_longest
except Exception:
    pass
_pm_earlier = [
    'from itertools import (accumulate, batched, chain, combinations,\n                       combinations_with_replacement, compress, count, cycle,\n                       dropwhile, filterfalse, groupby, islice, pairwise,\n                       permutations, product, repeat, starmap, takewhile, tee,\n                       zip_longest)',
]
def _pm_run_earlier(_srcs, _g):
    import sys as _s, io as _io, warnings as _w
    for _src in _srcs:
        _o, _e = _s.stdout, _s.stderr
        _s.stdout = _s.stderr = _io.StringIO()
        try:
            with _w.catch_warnings():
                _w.simplefilter('ignore')
                exec(compile(_src, '<earlier block>', 'exec'), _g)
        except (Exception, SystemExit):
            pass
        finally:
            _s.stdout, _s.stderr = _o, _e
    if 'matplotlib.pyplot' in _s.modules:
        _s.modules['matplotlib.pyplot'].close('all')   # their plots belong to their own blocks
_pm_run_earlier(_pm_earlier, globals())
del _pm_earlier, _pm_run_earlier

By default it adds: a running sum. Pass a function to change that: max gives the running maximum, mul the running product. initial= puts a starting value first, such as an opening bank balance.

7. Group runs

groupby() groups runs of equal items that sit next to each other. It gives pairs of (key, group), and each group is an iterator.

python
print([(k, len(list(g))) for k, g in groupby("aaabca")])
# [('a', 3), ('b', 1), ('c', 1), ('a', 1)]
print([k for k, g in groupby(sorted("abab"))])    # ['a', 'b']
+ setup added so this can run · re-runs 1 earlier block on this page first
# Lightweight mock for objects whose attributes/methods aren't critical
class _AutoMock:
    def __init__(self, name='mock'): self._name = name
    def __getattr__(self, k): return _AutoMock(self._name + '.' + k)
    def __call__(self, *a, **kw):
        print('-> ' + self._name + '() called')
        return _AutoMock(self._name + '()')
    def __repr__(self): return '<mock ' + self._name + '>'
    def __str__(self): return '<mock ' + self._name + '>'
    def __bool__(self): return True
    def __iter__(self): return iter([])
    def __len__(self): return 0
    def __getitem__(self, k): return _AutoMock(self._name + '[...]')
    def __setitem__(self, k, v): pass
    def __enter__(self): return self
    def __exit__(self, *a): return False
    async def __aenter__(self): return self
    async def __aexit__(self, *a): return False
    def __add__(self, o): return self
    def __radd__(self, o): return self
    def __sub__(self, o): return self
    def __mul__(self, o): return self
    def __rmul__(self, o): return self
    def __truediv__(self, o): return self
    def __eq__(self, o): return isinstance(o, _AutoMock)
    def __hash__(self): return hash(self._name)
    def __lt__(self, o): return True
    def __le__(self, o): return True
    def __gt__(self, o): return False
    def __ge__(self, o): return False
    def __mro_entries__(self, bases): return (object,)

def groupby(*_a, **_kw):
    print('-> groupby() called')
    return _AutoMock('groupby()')

# Earlier blocks on this page that this one builds on, re-run quietly first.
try:
    from itertools import accumulate, batched, chain, combinations, combinations_with_replacement, compress, count, cycle, dropwhile, filterfalse, groupby, islice, pairwise, permutations, product, repeat, starmap, takewhile, tee, zip_longest
except Exception:
    pass
_pm_earlier = [
    'from itertools import (accumulate, batched, chain, combinations,\n                       combinations_with_replacement, compress, count, cycle,\n                       dropwhile, filterfalse, groupby, islice, pairwise,\n                       permutations, product, repeat, starmap, takewhile, tee,\n                       zip_longest)',
]
def _pm_run_earlier(_srcs, _g):
    import sys as _s, io as _io, warnings as _w
    for _src in _srcs:
        _o, _e = _s.stdout, _s.stderr
        _s.stdout = _s.stderr = _io.StringIO()
        try:
            with _w.catch_warnings():
                _w.simplefilter('ignore')
                exec(compile(_src, '<earlier block>', 'exec'), _g)
        except (Exception, SystemExit):
            pass
        finally:
            _s.stdout, _s.stderr = _o, _e
    if 'matplotlib.pyplot' in _s.modules:
        _s.modules['matplotlib.pyplot'].close('all')   # their plots belong to their own blocks
_pm_run_earlier(_pm_earlier, globals())
del _pm_earlier, _pm_run_earlier

The 'a' at the end is a new group, because it is not next to the first run. To get one group per key, sort on the same key first:

python
words = ["fig", "kiwi", "plum", "pea"]
words.sort(key=len)
print({n: list(g) for n, g in groupby(words, len)})
# {3: ['fig', 'pea'], 4: ['kiwi', 'plum']}
+ setup added so this can run · re-runs 1 earlier block on this page first
# Lightweight mock for objects whose attributes/methods aren't critical
class _AutoMock:
    def __init__(self, name='mock'): self._name = name
    def __getattr__(self, k): return _AutoMock(self._name + '.' + k)
    def __call__(self, *a, **kw):
        print('-> ' + self._name + '() called')
        return _AutoMock(self._name + '()')
    def __repr__(self): return '<mock ' + self._name + '>'
    def __str__(self): return '<mock ' + self._name + '>'
    def __bool__(self): return True
    def __iter__(self): return iter([])
    def __len__(self): return 0
    def __getitem__(self, k): return _AutoMock(self._name + '[...]')
    def __setitem__(self, k, v): pass
    def __enter__(self): return self
    def __exit__(self, *a): return False
    async def __aenter__(self): return self
    async def __aexit__(self, *a): return False
    def __add__(self, o): return self
    def __radd__(self, o): return self
    def __sub__(self, o): return self
    def __mul__(self, o): return self
    def __rmul__(self, o): return self
    def __truediv__(self, o): return self
    def __eq__(self, o): return isinstance(o, _AutoMock)
    def __hash__(self): return hash(self._name)
    def __lt__(self, o): return True
    def __le__(self, o): return True
    def __gt__(self, o): return False
    def __ge__(self, o): return False
    def __mro_entries__(self, bases): return (object,)

def groupby(*_a, **_kw):
    print('-> groupby() called')
    return _AutoMock('groupby()')

# Earlier blocks on this page that this one builds on, re-run quietly first.
try:
    from itertools import accumulate, batched, chain, combinations, combinations_with_replacement, compress, count, cycle, dropwhile, filterfalse, groupby, islice, pairwise, permutations, product, repeat, starmap, takewhile, tee, zip_longest
except Exception:
    pass
_pm_earlier = [
    'from itertools import (accumulate, batched, chain, combinations,\n                       combinations_with_replacement, compress, count, cycle,\n                       dropwhile, filterfalse, groupby, islice, pairwise,\n                       permutations, product, repeat, starmap, takewhile, tee,\n                       zip_longest)',
]
def _pm_run_earlier(_srcs, _g):
    import sys as _s, io as _io, warnings as _w
    for _src in _srcs:
        _o, _e = _s.stdout, _s.stderr
        _s.stdout = _s.stderr = _io.StringIO()
        try:
            with _w.catch_warnings():
                _w.simplefilter('ignore')
                exec(compile(_src, '<earlier block>', 'exec'), _g)
        except (Exception, SystemExit):
            pass
        finally:
            _s.stdout, _s.stderr = _o, _e
    if 'matplotlib.pyplot' in _s.modules:
        _s.modules['matplotlib.pyplot'].close('all')   # their plots belong to their own blocks
_pm_run_earlier(_pm_earlier, globals())
del _pm_earlier, _pm_run_earlier

8. Map, copy, fold

python
from functools import reduce
from operator import itemgetter

print(list(starmap(pow, [(2, 3), (3, 2)])))                     # [8, 9]
print(reduce(mul, [2, 3, 4]))                                   # 24
print(sorted([("a", 2), ("b", 1)], key=itemgetter(1)))          # [('b', 1), ('a', 2)]

a, b = tee(iter([1, 2, 3]))
next(b, None)
print(list(zip(a, b)))                                          # [(1, 2), (2, 3)]
+ setup added so this can run · re-runs 2 earlier blocks on this page first
# Lightweight mock for objects whose attributes/methods aren't critical
class _AutoMock:
    def __init__(self, name='mock'): self._name = name
    def __getattr__(self, k): return _AutoMock(self._name + '.' + k)
    def __call__(self, *a, **kw):
        print('-> ' + self._name + '() called')
        return _AutoMock(self._name + '()')
    def __repr__(self): return '<mock ' + self._name + '>'
    def __str__(self): return '<mock ' + self._name + '>'
    def __bool__(self): return True
    def __iter__(self): return iter([])
    def __len__(self): return 0
    def __getitem__(self, k): return _AutoMock(self._name + '[...]')
    def __setitem__(self, k, v): pass
    def __enter__(self): return self
    def __exit__(self, *a): return False
    async def __aenter__(self): return self
    async def __aexit__(self, *a): return False
    def __add__(self, o): return self
    def __radd__(self, o): return self
    def __sub__(self, o): return self
    def __mul__(self, o): return self
    def __rmul__(self, o): return self
    def __truediv__(self, o): return self
    def __eq__(self, o): return isinstance(o, _AutoMock)
    def __hash__(self): return hash(self._name)
    def __lt__(self, o): return True
    def __le__(self, o): return True
    def __gt__(self, o): return False
    def __ge__(self, o): return False
    def __mro_entries__(self, bases): return (object,)

def tee(*_a, **_kw):
    print('-> tee() called')
    return _AutoMock('tee()')
mul = _AutoMock('mul')
def starmap(*_a, **_kw):
    print('-> starmap() called')
    return _AutoMock('starmap()')
def accumulate(*_a, **_kw):
    print('-> accumulate() called')
    return _AutoMock('accumulate()')

# Earlier blocks on this page that this one builds on, re-run quietly first.
try:
    from itertools import accumulate, batched, chain, combinations, combinations_with_replacement, compress, count, cycle, dropwhile, filterfalse, groupby, islice, pairwise, permutations, product, repeat, starmap, takewhile, tee, zip_longest
    from operator import mul
except Exception:
    pass
_pm_earlier = [
    'from itertools import (accumulate, batched, chain, combinations,\n                       combinations_with_replacement, compress, count, cycle,\n                       dropwhile, filterfalse, groupby, islice, pairwise,\n                       permutations, product, repeat, starmap, takewhile, tee,\n                       zip_longest)',
    'from operator import mul\n\nprint(list(accumulate([1, 2, 3, 4])))             # [1, 3, 6, 10]\nprint(list(accumulate([3, 1, 5, 2], max)))        # [3, 3, 5, 5]\nprint(list(accumulate([1, 2, 3], mul)))           # [1, 2, 6]\nprint(list(accumulate([5, -2], initial=100)))     # [100, 105, 103]',
]
def _pm_run_earlier(_srcs, _g):
    import sys as _s, io as _io, warnings as _w
    for _src in _srcs:
        _o, _e = _s.stdout, _s.stderr
        _s.stdout = _s.stderr = _io.StringIO()
        try:
            with _w.catch_warnings():
                _w.simplefilter('ignore')
                exec(compile(_src, '<earlier block>', 'exec'), _g)
        except (Exception, SystemExit):
            pass
        finally:
            _s.stdout, _s.stderr = _o, _e
    if 'matplotlib.pyplot' in _s.modules:
        _s.modules['matplotlib.pyplot'].close('all')   # their plots belong to their own blocks
_pm_run_earlier(_pm_earlier, globals())
del _pm_earlier, _pm_run_earlier

starmap() unpacks each tuple into the function's arguments: pow(2, 3), then pow(3, 2). reduce() combines all the items into one value. itemgetter(1) picks item 1 of each tuple, a quick key function for sorting. tee() makes two independent copies of one iterator, so each copy can move on its own. Here b moves one step ahead, so zip(a, b) pairs each item with the next one.

9. Built-in partners

These are not in itertools, but you use them in the same way.

python
print(list(enumerate("ab", start=1)))                 # [(1, 'a'), (2, 'b')]
print(dict(zip("ab", [1, 2])))                        # {'a': 1, 'b': 2}
print(list(zip(*[(1, "a"), (2, "b")])))               # [(1, 2), ('a', 'b')]
print(list(filter(None, [0, 1, "", "x"])))            # [1, 'x']
print(sum(x * x for x in range(4)))                   # 14
print(any(n > 3 for n in [1, 5, 2]))                  # True
print(all(n > 0 for n in []))                         # True
print(next((n for n in [1, 6, 8] if n > 5), None))    # 6

zip(*rows) turns rows into columns ("unzip"). filter(None, ...) drops 0, "", None and other values that count as false. sum(x * x for x in ...) uses a generator expression (like a list comprehension, but in round brackets). It makes the items one at a time, so no list is built. any() stops at the first True. all() of an empty input is True. next(..., None) gives the first match, or None if there is no match.

10. Recipes

python
print(list(dict.fromkeys("abcab")))           # ['a', 'b', 'c']
print(list(zip(*[iter(range(6))] * 3)))       # [(0, 1, 2), (3, 4, 5)]
s = "abcd"
print(list(zip(s, s[1:], s[2:])))             # [('a', 'b', 'c'), ('b', 'c', 'd')]

dict.fromkeys() drops duplicates and keeps the order. The zip(*[iter(...)] * 3) line makes chunks of 3 in Python before 3.12, but it drops any leftover items. The last line is a sliding window of 3 over a list or string.

For any iterable, not only lists, this generator function (a function that uses yield) keeps the first copy of each item. key= decides which items count as the same:

python
def unique_everseen(items, key=None):
    seen = set()
    for x in items:
        k = x if key is None else key(x)
        if k not in seen:
            seen.add(k)
            yield x

print(list(unique_everseen("AAbBC", key=str.lower)))  # ['A', 'b', 'C']

Common mistakes

Using an iterator twice. An iterator is used up after one pass, so the second list() is empty:

python
rows = map(str, [1, 2])
print(list(rows))   # ['1', '2']
print(list(rows))   # []

Fix: make a list once (rows = list(map(str, nums))) and reuse the list.

groupby without sorting. The same key comes back as a new group: [k for k, g in groupby("abab")] gives ['a', 'b', 'a', 'b']. Fix: sort on the same key first.

Keeping groupby groups for later. Each group g is empty once the loop moves on to the next key:

python
groups = [g for k, g in groupby("aab")]
print([list(g) for g in groups])                  # [[], []]
print([list(g) for k, g in groupby("aab")])       # [['a', 'a'], ['b']]
+ setup added so this can run · re-runs 1 earlier block on this page first
# Lightweight mock for objects whose attributes/methods aren't critical
class _AutoMock:
    def __init__(self, name='mock'): self._name = name
    def __getattr__(self, k): return _AutoMock(self._name + '.' + k)
    def __call__(self, *a, **kw):
        print('-> ' + self._name + '() called')
        return _AutoMock(self._name + '()')
    def __repr__(self): return '<mock ' + self._name + '>'
    def __str__(self): return '<mock ' + self._name + '>'
    def __bool__(self): return True
    def __iter__(self): return iter([])
    def __len__(self): return 0
    def __getitem__(self, k): return _AutoMock(self._name + '[...]')
    def __setitem__(self, k, v): pass
    def __enter__(self): return self
    def __exit__(self, *a): return False
    async def __aenter__(self): return self
    async def __aexit__(self, *a): return False
    def __add__(self, o): return self
    def __radd__(self, o): return self
    def __sub__(self, o): return self
    def __mul__(self, o): return self
    def __rmul__(self, o): return self
    def __truediv__(self, o): return self
    def __eq__(self, o): return isinstance(o, _AutoMock)
    def __hash__(self): return hash(self._name)
    def __lt__(self, o): return True
    def __le__(self, o): return True
    def __gt__(self, o): return False
    def __ge__(self, o): return False
    def __mro_entries__(self, bases): return (object,)

def groupby(*_a, **_kw):
    print('-> groupby() called')
    return _AutoMock('groupby()')

# Earlier blocks on this page that this one builds on, re-run quietly first.
try:
    from itertools import accumulate, batched, chain, combinations, combinations_with_replacement, compress, count, cycle, dropwhile, filterfalse, groupby, islice, pairwise, permutations, product, repeat, starmap, takewhile, tee, zip_longest
except Exception:
    pass
_pm_earlier = [
    'from itertools import (accumulate, batched, chain, combinations,\n                       combinations_with_replacement, compress, count, cycle,\n                       dropwhile, filterfalse, groupby, islice, pairwise,\n                       permutations, product, repeat, starmap, takewhile, tee,\n                       zip_longest)',
]
def _pm_run_earlier(_srcs, _g):
    import sys as _s, io as _io, warnings as _w
    for _src in _srcs:
        _o, _e = _s.stdout, _s.stderr
        _s.stdout = _s.stderr = _io.StringIO()
        try:
            with _w.catch_warnings():
                _w.simplefilter('ignore')
                exec(compile(_src, '<earlier block>', 'exec'), _g)
        except (Exception, SystemExit):
            pass
        finally:
            _s.stdout, _s.stderr = _o, _e
    if 'matplotlib.pyplot' in _s.modules:
        _s.modules['matplotlib.pyplot'].close('all')   # their plots belong to their own blocks
_pm_run_earlier(_pm_earlier, globals())
del _pm_earlier, _pm_run_earlier

Fix: call list(g) inside the loop.

Printing an iterator. print(chain("ab")) shows <itertools.chain object at ...>, not the items. Fix: print(list(chain("ab"))).

list() of an endless iterator. list(count()) never ends and fills the memory. Fix: use islice(), or break out of a loop.

Indexing an iterator. chain([1, 2])[0] raises TypeError, because iterators have no positions. Fix: use list(...)[0] or next(islice(it, 0, None)).

Expecting equal items to be skipped. combinations() picks by position, not by value, so equal items still form a pair: list(combinations([1, 1], 2)) is [(1, 1)].

See also

Practice this

on practicepython.in

Short exercises that run in your browser and tell you what your code did, as well as whether a test passed.

Found this useful? Share it with a friend who’s learning Python.