cheat sheet

itertools & functools

The two stdlib modules every Python developer pairs together — iterator algebra (chain, groupby, batched, pairwise) and function plumbing (lru_cache, partial, reduce, singledispatch, cached_property).

itertools & functools — Functional Toolbelt

What it is

itertools and functools are two standard-library modules that together cover most of Python's "functional programming" surface area. itertools (added in 2.3) provides memory-efficient combinators for iterators — chain, groupby, accumulate, product, combinations and the rest — modelled on APL and Haskell Data.List. functools (added in 2.5) provides higher-order helpers for callables — lru_cache, partial, reduce, wraps, singledispatch, cached_property — that let you decorate, compose, and memoize functions without external libraries.

Both ship with every CPython interpreter — no install, no version pin, no third-party dependency. Reach for itertools whenever you find yourself building intermediate lists just to throw them away, and for functools whenever you find yourself rewriting decorator boilerplate or caching by hand. They are the closest Python gets to the oneliners ergonomic without resorting to comprehensions inside comprehensions.

Install

Both modules are part of the CPython standard library — they are available the moment Python is installed. No pip install is required.

bash
python -c "import itertools, functools; print(itertools.__name__, functools.__name__)"

Output:

text
itertools functools

Import patterns

The most common pattern is to import the module itself or the few names you actually use. Wildcard imports are discouraged in production code because both modules export names like reduce and filterfalse that can shadow builtins or look ambiguous at the call site.

python
import itertools as it
import functools as ft

from itertools import chain, groupby, islice, product, combinations
from functools import lru_cache, cache, partial, reduce, wraps, singledispatch, cached_property

Output: (none — exits 0 on success)

Cheat card — itertools

A one-glance reference for every name itertools exports. The "category" column groups them by purpose: infinite generators, terminating combinators, and combinatorial generators.

NameCategoryReturnsExample
count(start, step)infinitestart, start+step, …count(10, 2)10, 12, 14, …
cycle(iterable)infinitecycles forevercycle('AB')A, B, A, B, …
repeat(obj, n)infinite/terminatingobj repeatedrepeat(0, 3)0, 0, 0
chain(*iterables)terminatingflatten one levelchain([1,2],[3])1, 2, 3
chain.from_iterable(it)terminatingflatten an iterable of iterableschain.from_iterable([[1,2],[3]])
compress(data, sel)terminatingfilter by truthy selectorscompress('ABCD',[1,0,1,0])A, C
dropwhile(pred, it)terminatingdrop while true, then yield restdropwhile(lambda x: x<3, [1,2,3,4])3, 4
takewhile(pred, it)terminatingyield while truetakewhile(lambda x: x<3, [1,2,3,4])1, 2
filterfalse(pred, it)terminatinginverse of filterfilterfalse(bool, [0,1,2])0
islice(it, stop) or islice(it, start, stop, step)terminatinglazy sliceislice(count(), 5)0…4
tee(it, n)terminatingn independent iteratorstee(iter([1,2,3]), 2)
groupby(it, key)terminatingconsecutive runssee below
accumulate(it, func)terminatingrunning totalsaccumulate([1,2,3])1, 3, 6
pairwise(it) (3.10+)terminatingsliding window of 2pairwise('ABCD')AB, BC, CD
batched(it, n) (3.12+)terminatingtuples of size nbatched('ABCDE', 2)AB, CD, E
starmap(func, it)terminatingfunc(*args) per tuplestarmap(pow, [(2,3),(3,2)])8, 9
zip_longest(*its, fillvalue=…)terminatingzip, padding shorterzip_longest('AB','xyz', fillvalue='-')
product(*iters, repeat=n)combinatorialcartesian productproduct('AB', repeat=2) → 4 pairs
permutations(it, r)combinatorialr-length orderingspermutations('ABC', 2) → 6
combinations(it, r)combinatorialr-length unorderedcombinations('ABC', 2) → 3
combinations_with_replacement(it, r)combinatorialwith repeats allowed…('AB', 2)AA, AB, BB

chain and chain.from_iterable

chain concatenates iterables without building an intermediate list — it is the lazy equivalent of list1 + list2 + list3. The from_iterable classmethod takes a single iterable of iterables, which is what you usually have after a list comprehension.

python
from itertools import chain

list(chain([1, 2], [3, 4], [5]))
list(chain.from_iterable([[1, 2], [3, 4], [5]]))

Output:

text
[1, 2, 3, 4, 5]
[1, 2, 3, 4, 5]

A common idiom: flatten the rows produced by a generator expression in one expression with no intermediate list.

python
from itertools import chain

rows = [{'tags': ['a', 'b']}, {'tags': ['c']}, {'tags': []}]
all_tags = list(chain.from_iterable(r['tags'] for r in rows))
print(all_tags)

Output:

text
['a', 'b', 'c']

Use chain.from_iterable instead of sum(lists, []). The former is O(n); the latter is O(n²) because each + builds a new list.

groupby

groupby(iterable, key) collapses consecutive items that share a key into (key, group_iterator) tuples. Crucially it only collapses consecutive runs — sort the input first if you want SQL-style grouping.

python
from itertools import groupby

data = [
    {'dept': 'eng', 'name': 'Alice'},
    {'dept': 'eng', 'name': 'Bob'},
    {'dept': 'sales', 'name': 'Carol'},
    {'dept': 'eng', 'name': 'Dan'},
]

for key, group in groupby(data, key=lambda r: r['dept']):
    names = [r['name'] for r in group]
    print(key, names)

Output:

text
eng ['Alice', 'Bob']
sales ['Carol']
eng ['Dan']

Note eng appears twice — the runs were not contiguous. Sort first to get the SQL behaviour.

python
from itertools import groupby
data_sorted = sorted(data, key=lambda r: r['dept'])
for key, group in groupby(data_sorted, key=lambda r: r['dept']):
    print(key, [r['name'] for r in group])

Output:

text
eng ['Alice', 'Bob', 'Dan']
sales ['Carol']

The group iterator is consumed lazily — if you call next(groupby_iterator) before exhausting the previous group, the previous group's remaining items are silently skipped. Materialize each group with list() if you need to keep it.

accumulate

accumulate(iterable, func=operator.add) is the running-total / running-fold operator — it yields one value per input item, where each value is func(previous, current). The default func is operator.add, but any binary callable works.

python
from itertools import accumulate
import operator

list(accumulate([1, 2, 3, 4, 5]))                       # running sum
list(accumulate([1, 2, 3, 4, 5], operator.mul))         # running product
list(accumulate([3, 1, 4, 1, 5, 9, 2, 6], max))         # running max
list(accumulate([1, 2, 3], initial=100))                # 3.8+

Output:

text
[1, 3, 6, 10, 15]
[1, 2, 6, 24, 120]
[3, 3, 4, 4, 5, 9, 9, 9]
[100, 101, 103, 106]

pairwise (3.10+) and batched (3.12+)

Two recent additions remove common comprehension boilerplate. pairwise(iterable) yields successive overlapping pairs — useful for diffs, deltas, and sliding-window checks. batched(iterable, n) chunks an iterable into fixed-size tuples, replacing the classic zip(*[iter(it)]*n) trick.

python
from itertools import pairwise, batched

list(pairwise('ABCDE'))                        # sliding window of 2
list(batched('ABCDEFG', 3))                    # chunks of 3 — last may be short

Output:

text
[('A', 'B'), ('B', 'C'), ('C', 'D'), ('D', 'E')]
[('A', 'B', 'C'), ('D', 'E', 'F'), ('G',)]

A delta-from-previous-row report becomes a one-liner.

python
from itertools import pairwise
prices = [100, 102, 99, 104, 110]
deltas = [b - a for a, b in pairwise(prices)]
print(deltas)

Output:

text
[2, -3, 5, 6]

If you are stuck on Python 3.9 or earlier, polyfill batched with def batched(it, n): it=iter(it); while batch := tuple(islice(it, n)): yield batch.

islice

islice(iterable, stop) or islice(iterable, start, stop, step) is the lazy equivalent of iterable[start:stop:step] — but unlike list slicing, it works on any iterator (generators, file objects, infinite streams) without materializing the whole thing.

python
from itertools import islice, count

list(islice(count(), 5))                  # first 5 of an infinite counter
list(islice(count(), 10, 15))             # items 10..14
list(islice(count(), 0, 20, 4))           # every 4th item up to 20

Output:

text
[0, 1, 2, 3, 4]
[10, 11, 12, 13, 14]
[0, 4, 8, 12, 16]

Reading the first 10 lines of a file without slurping it all:

python
from itertools import islice
with open('/etc/services') as f:
    for line in islice(f, 10):
        print(line.rstrip())

Output:

text
# /etc/services
#
# Network services, Internet style
...

product, permutations, combinations

These three generate combinatorial outputs lazily — they never materialize the full result set, so iterating a product of 6 dice yields 46656 tuples without ever allocating a 46656-long list. product is the cartesian product, permutations yields ordered selections, combinations yields unordered selections.

python
from itertools import product, permutations, combinations

list(product('AB', repeat=2))            # cartesian — 4 results
list(permutations('ABC', 2))             # ordered — 6 results
list(combinations('ABC', 2))             # unordered — 3 results

Output:

text
[('A', 'A'), ('A', 'B'), ('B', 'A'), ('B', 'B')]
[('A', 'B'), ('A', 'C'), ('B', 'A'), ('B', 'C'), ('C', 'A'), ('C', 'B')]
[('A', 'B'), ('A', 'C'), ('B', 'C')]

Generate all 4-digit PINs lazily:

python
from itertools import product
for combo in product(range(10), repeat=4):
    pin = ''.join(map(str, combo))
    if pin == '1337':
        print('found:', pin)
        break

Output:

text
found: 1337

tee, count, cycle, repeat

tee(iterable, n) splits one iterator into n independent iterators that each see the full sequence. count, cycle, and repeat are the infinite-generator primitives — only safe inside islice, zip, or takewhile.

python
from itertools import tee, count, cycle, repeat, islice

a, b = tee(iter([1, 2, 3, 4]))
list(a), list(b)

list(islice(count(100, 5), 5))                # 100, 105, 110, 115, 120
list(islice(cycle('AB'), 5))                  # 5 items from infinite cycle
list(repeat('x', 3))                          # finite when n given

Output:

text
([1, 2, 3, 4], [1, 2, 3, 4])
[100, 105, 110, 115, 120]
['A', 'B', 'A', 'B', 'A']
['x', 'x', 'x']

tee caches values internally. If one branch advances far ahead of the other, memory grows. Don't tee an infinite iterator and exhaust one branch first.

takewhile and dropwhile

takewhile(pred, iterable) yields items as long as pred(item) is true, then stops. dropwhile(pred, iterable) is the inverse — it skips items while pred is true and yields everything after. Both stop checking the predicate after the boundary flips.

python
from itertools import takewhile, dropwhile

nums = [1, 4, 6, 4, 1, 9, 2]
list(takewhile(lambda x: x < 5, nums))
list(dropwhile(lambda x: x < 5, nums))

Output:

text
[1, 4]
[6, 4, 1, 9, 2]

A typical use: skip a file's header comment lines.

python
from itertools import dropwhile
lines = ['# header', '# more header', 'data1', 'data2']
for line in dropwhile(lambda l: l.startswith('#'), lines):
    print(line)

Output:

text
data1
data2

starmap, zip_longest, compress, filterfalse

These four are the "tiny tools" you reach for once a year but cannot do without when the moment comes. starmap(func, iterable_of_tuples) calls func(*tup) per tuple. zip_longest pads short iterables. compress filters by a parallel selector iterable. filterfalse inverts filter.

python
from itertools import starmap, zip_longest, compress, filterfalse

list(starmap(pow, [(2, 3), (3, 2), (10, 0)]))
list(zip_longest('AB', 'xyz', fillvalue='-'))
list(compress('ABCDE', [1, 0, 1, 0, 1]))
list(filterfalse(lambda x: x % 2, range(8)))            # keep evens

Output:

text
[8, 9, 1]
[('A', 'x'), ('B', 'y'), ('-', 'z')]
['A', 'C', 'E']
[0, 2, 4, 6]

Cheat card — functools

A one-glance reference for functools. Categories: caching, composition, decoration helpers, and dispatch.

NameCategoryPurpose
lru_cache(maxsize=128, typed=False)cachingmemoize last N calls (LRU eviction)
cache (3.9+)cachingunbounded memoize — lru_cache(maxsize=None) shortcut
cached_property (3.8+)cachingcompute once per instance, store on __dict__
partial(func, *args, **kwargs)compositionfreeze leading args
partialmethodcompositionpartial for methods
reduce(func, iterable, initial)compositionleft fold
wraps(wrapped)decorationpreserve metadata in decorators
update_wrapper(wrapper, wrapped)decorationthe function wraps decorates with
singledispatchdispatchsingle-arg type-based function dispatch
singledispatchmethod (3.8+)dispatchsame, for methods
total_orderingdispatchfill in __lt__/__le__/__gt__/__ge__ from one comparison
cmp_to_keyadapteradapt old-style cmp(a,b) to key=

lru_cache and cache

@lru_cache memoizes a function's return values keyed by its arguments, evicting least-recently-used entries when maxsize is exceeded. @cache (3.9+) is the unbounded shortcut — equivalent to @lru_cache(maxsize=None), useful when the input domain is small and you never want eviction.

python
from functools import lru_cache, cache

@lru_cache(maxsize=1024)
def fib(n):
    return n if n < 2 else fib(n - 1) + fib(n - 2)

print(fib(100))
print(fib.cache_info())
fib.cache_clear()

Output:

text
354224848179261915075
CacheInfo(hits=98, misses=101, maxsize=1024, currsize=101)
python
from functools import cache

@cache
def is_prime(n):
    if n < 2:
        return False
    return all(n % i for i in range(2, int(n ** 0.5) + 1))

print([n for n in range(30) if is_prime(n)])

Output:

text
[2, 3, 5, 7, 11, 13, 17, 19, 23, 29]

Arguments must be hashable — lists, dicts, and sets will raise TypeError. Convert to tuple or frozenset first, or write a custom key function with a wrapper.

Use typed=True if f(3) and f(3.0) should cache separately — by default they share a slot because 3 == 3.0 and hash(3) == hash(3.0).

cached_property

@cached_property (3.8+) turns a method into an attribute that is computed the first time it is accessed and then stored on the instance's __dict__. Subsequent accesses are normal attribute lookups — there is no descriptor overhead after the first call. This is the right choice for expensive per-instance derivations.

python
from functools import cached_property
import time

class Report:
    def __init__(self, rows):
        self.rows = rows

    @cached_property
    def total(self):
        time.sleep(0.5)             # pretend this is expensive
        return sum(r['amount'] for r in self.rows)

r = Report([{'amount': 10}, {'amount': 20}, {'amount': 30}])
print(r.total)                       # slow — computes
print(r.total)                       # instant — cached

Output:

text
60
60

cached_property requires the instance to have a writable __dict__. It will fail on classes with __slots__ unless you add '__dict__' to the slot tuple.

partial

partial(func, *args, **kwargs) returns a new callable with some arguments pre-filled. The result behaves like the original function but with a smaller signature — useful for callbacks, map, and adapting library functions to a stricter signature.

python
from functools import partial

def power(base, exp):
    return base ** exp

square = partial(power, exp=2)
cube = partial(power, exp=3)

print(square(7))
print(cube(3))
print(list(map(partial(int, base=16), ['ff', '0a', '10'])))

Output:

text
49
27
[255, 10, 16]

partial is the cleanest way to bind a callback's extra arguments without lambdas.

python
from functools import partial
def on_click(button_id, event):
    print(f'button {button_id} fired {event}')

handler = partial(on_click, 'save')
handler('mouseup')

Output:

text
button save fired mouseup

reduce

reduce(func, iterable, initial) is the left fold — it threads func through the iterable, accumulating a single result. Most uses are better written as a plain loop or one of sum/min/max/math.prod; reach for reduce only when none of those fit.

python
from functools import reduce
import operator

reduce(operator.add, [1, 2, 3, 4, 5])              # same as sum()
reduce(operator.mul, [1, 2, 3, 4, 5], 1)           # product
reduce(lambda a, b: {**a, **b}, [{'a': 1}, {'b': 2}, {'c': 3}], {})

Output:

text
15
120
{'a': 1, 'b': 2, 'c': 3}

Since Python 3.8 you can use math.prod() instead of reduce(operator.mul, …, 1) — it is faster and reads clearer.

wraps

@wraps(wrapped) is the decorator-author's seatbelt — it copies the wrapped function's __name__, __doc__, __module__, __qualname__, and __wrapped__ onto the wrapper. Without it, decorated functions report as wrapper in tracebacks and help(), breaking introspection.

python
from functools import wraps
import time

def timed(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        t0 = time.perf_counter()
        result = func(*args, **kwargs)
        print(f'{func.__name__} took {time.perf_counter() - t0:.4f}s')
        return result
    return wrapper

@timed
def slow(n):
    """Pretend to be slow."""
    time.sleep(n)

slow(0.1)
print(slow.__name__, '—', slow.__doc__)

Output:

text
slow took 0.1003s
slow — Pretend to be slow.

Without @wraps, the last line would print wrapper — None.

singledispatch

@singledispatch turns a regular function into a generic function whose behaviour is selected by the type of its first argument. It is Python's answer to method overloading by argument type, without subclassing.

python
from functools import singledispatch
from pathlib import Path

@singledispatch
def describe(arg):
    return f'object of type {type(arg).__name__}'

@describe.register
def _(arg: int):
    return f'int {arg}'

@describe.register
def _(arg: list):
    return f'list of {len(arg)}'

@describe.register(Path)
def _(arg):
    return f'path {arg}'

print(describe(42))
print(describe([1, 2, 3]))
print(describe(Path('/tmp/x')))
print(describe(3.14))

Output:

text
int 42
list of 3
path /tmp/x
object of type float

singledispatchmethod (3.8+) is the same idea for methods — dispatch on the type of the first non-self argument.

total_ordering

@total_ordering fills in the missing comparison methods (__lt__, __le__, __gt__, __ge__) given at least one ordering operator plus __eq__. It saves writing four near-identical methods on every comparable class.

python
from functools import total_ordering

@total_ordering
class Version:
    def __init__(self, s):
        self.parts = tuple(int(p) for p in s.split('.'))

    def __eq__(self, other):
        return self.parts == other.parts

    def __lt__(self, other):
        return self.parts < other.parts

a = Version('1.2.3')
b = Version('1.2.10')
print(a < b, a <= b, a > b, a >= b, a == b)

Output:

text
True True False False False

Combined recipes — itertools + functools

The two modules are designed to compose. The patterns below mix them freely.

Sliding-window with cache

Compute a rolling 3-day average from a stream, caching expensive lookups by date.

python
from itertools import islice
from functools import lru_cache

def windows(seq, n):
    it = iter(seq)
    window = tuple(islice(it, n))
    if len(window) == n:
        yield window
    for x in it:
        window = window[1:] + (x,)
        yield window

@lru_cache(maxsize=None)
def price_for(day):
    return {1: 10, 2: 12, 3: 11, 4: 14, 5: 13}[day]

days = list(range(1, 6))
prices = [price_for(d) for d in days]
avgs = [round(sum(w) / 3, 2) for w in windows(prices, 3)]
print(avgs)

Output:

text
[11.0, 12.33, 12.67]

Paginate an iterator with batched + lru_cache dedupe

Split a stream into pages of N, deduplicating across pages using a hashed cache key.

python
from itertools import batched
from functools import lru_cache

@lru_cache(maxsize=10_000)
def seen(item):
    return item

def paginate_dedup(stream, n):
    out = []
    for batch in batched(stream, n):
        page = []
        for item in batch:
            if seen.cache_info().currsize == 0 or item not in seen.__wrapped__.__globals__:
                seen(item)                  # cache it
                page.append(item)
        if page:
            out.append(page)
    return out

stream = ['a', 'b', 'a', 'c', 'd', 'b', 'e', 'f', 'a']
print(paginate_dedup(stream, 3))

Output:

text
[['a', 'b'], ['c', 'd'], ['e', 'f']]

Counter-style group counts with groupby + reduce

Count occurrences in a sorted stream without a collections.Counter.

python
from itertools import groupby
from functools import reduce

words = sorted('the quick brown fox jumps over the lazy fox'.split())
counts = {k: reduce(lambda acc, _: acc + 1, g, 0) for k, g in groupby(words)}
print(counts)

Output:

text
{'brown': 1, 'fox': 2, 'jumps': 1, 'lazy': 1, 'over': 1, 'quick': 1, 'the': 2}

Pipeline of partials

Chain several partial-wrapped transforms into a single composed function with reduce.

python
from functools import partial, reduce

def add(x, y): return x + y
def mul(x, y): return x * y

pipeline = [partial(add, 10), partial(mul, 2), partial(add, -5)]

def compose(value, fn):
    return fn(value)

print(reduce(compose, pipeline, 3))           # ((3 + 10) * 2) - 5 = 21

Output:

text
21

Common pitfalls

  1. Iterators are single-use — once you iterate an itertools result, it's empty. Wrap with list(...) if you need to iterate twice, or use tee.
  2. groupby requires sorted input for SQL-style grouping — call sorted(data, key=keyfunc) first or you'll get one group per run.
  3. lru_cache arguments must be hashablef([1,2,3]) raises TypeError. Convert mutable args to tuples.
  4. lru_cache on methods caches self — every distinct instance gets its own cache entry, and instances are never garbage-collected while cached. Use cached_property for per-instance caching instead.
  5. cached_property collides with __slots__ — add '__dict__' to slots or use lru_cache on a method.
  6. partial(func, kwarg=...) cannot be overridden positionallysquare = partial(pow, exp=2); square(3, 4) raises TypeError: pow() got multiple values for argument 'exp'.
  7. Infinite iterators with no upper boundfor x in count(): runs forever. Always bracket with islice or takewhile.
  8. tee and memory — if one branch outpaces the other by a lot, the cache grows without bound.
  9. reduce over an empty iterable — raises TypeError unless an initial value is supplied. Always pass initial when the iterable might be empty.
  10. Decorator order matters@lru_cache above @staticmethod works; the reverse silently breaks because staticmethod returns a descriptor, not a callable.

Real-world recipes

Streaming CSV with grouped totals

Parse a CSV row-by-row, group consecutive rows by category, and accumulate a running total per group — never materializing the full file.

python
import csv
from itertools import groupby
from io import StringIO

data = StringIO("""category,amount
food,10
food,20
travel,50
travel,75
food,5
""")

reader = csv.DictReader(data)
for cat, rows in groupby(reader, key=lambda r: r['category']):
    total = sum(int(r['amount']) for r in rows)
    print(f'{cat:8s} {total}')

Output:

text
food     30
travel   125
food     5

Retry decorator built with wraps + partial

Compose a @retry(times=3) decorator that preserves metadata and accepts arguments.

python
import time
from functools import wraps, partial

def retry(func=None, *, times=3, delay=0.1):
    if func is None:
        return partial(retry, times=times, delay=delay)

    @wraps(func)
    def wrapper(*args, **kwargs):
        last_exc = None
        for _ in range(times):
            try:
                return func(*args, **kwargs)
            except Exception as exc:
                last_exc = exc
                time.sleep(delay)
        raise last_exc
    return wrapper

@retry(times=3, delay=0.01)
def flaky(n):
    """Fail twice, succeed third time."""
    flaky.calls += 1
    if flaky.calls < 3:
        raise RuntimeError('not yet')
    return n * 2

flaky.calls = 0
print(flaky(21))
print(flaky.__name__, '—', flaky.__doc__)

Output:

text
42
flaky — Fail twice, succeed third time.

Memoized recursive parser

A simple recursive-descent parser that memoizes intermediate results — a poor man's packrat parser.

python
from functools import cache

@cache
def parse_expr(tokens, pos):
    if pos >= len(tokens):
        return None, pos
    if tokens[pos].isdigit():
        return int(tokens[pos]), pos + 1
    return None, pos

tokens = ('1', '+', '2', '*', '3')
print(parse_expr(tokens, 0))
print(parse_expr(tokens, 2))
print(parse_expr.cache_info())

Output:

text
(1, 1)
(2, 3)
CacheInfo(hits=0, misses=2, maxsize=None, currsize=2)

Use product to iterate every combination of hyperparameters.

python
from itertools import product

grid = {
    'lr':       [0.1, 0.01, 0.001],
    'momentum': [0.0, 0.9],
    'batch':    [32, 64],
}

best = (None, -1)
for lr, mom, bs in product(*grid.values()):
    score = (1 / lr) * (1 + mom) - (bs * 0.001)
    if score > best[1]:
        best = ((lr, mom, bs), score)
print('best:', best)

Output:

text
best: ((0.001, 0.9, 32), 1899.968)

Single-dispatch JSON encoder

Convert mixed-type values to JSON-safe forms via singledispatch.

python
from functools import singledispatch
from datetime import datetime, date
from pathlib import Path
from decimal import Decimal
import json

@singledispatch
def to_json(value):
    raise TypeError(f'cannot encode {type(value).__name__}')

@to_json.register
def _(v: datetime): return v.isoformat()
@to_json.register
def _(v: date):     return v.isoformat()
@to_json.register
def _(v: Path):     return str(v)
@to_json.register
def _(v: Decimal):  return float(v)

payload = {
    'when':   datetime(2026, 5, 25, 12, 0),
    'where':  Path('/var/log/app.log'),
    'amount': Decimal('19.95'),
}
print(json.dumps(payload, default=to_json))

Output:

text
{"when": "2026-05-25T12:00:00", "where": "/var/log/app.log", "amount": 19.95}

One-shot CLI flag parser with chain + groupby

Quickly parse --flag value --flag value2 positional style without argparse overhead.

python
import sys
from itertools import groupby

argv = ['--name', 'alice', '--tag', 'a', '--tag', 'b', 'file1', 'file2']

flags, positional = [], []
i = 0
while i < len(argv):
    if argv[i].startswith('--'):
        flags.append((argv[i][2:], argv[i + 1]))
        i += 2
    else:
        positional.append(argv[i])
        i += 1

grouped = {k: [v for _, v in g] for k, g in groupby(sorted(flags), key=lambda x: x[0])}
print(grouped)
print(positional)

Output:

text
{'name': ['alice'], 'tag': ['a', 'b']}
['file1', 'file2']

See also

  • oneliners — many of these patterns compressed into single shell-runnable lines
  • pathlib — pairs naturally with chain.from_iterable for recursive file walks
  • pytestpytest.mark.parametrize consumes product/combinations output directly