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.
python -c "import itertools, functools; print(itertools.__name__, functools.__name__)"
Output:
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.
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.
| Name | Category | Returns | Example |
|---|---|---|---|
count(start, step) | infinite | start, start+step, … | count(10, 2) → 10, 12, 14, … |
cycle(iterable) | infinite | cycles forever | cycle('AB') → A, B, A, B, … |
repeat(obj, n) | infinite/terminating | obj repeated | repeat(0, 3) → 0, 0, 0 |
chain(*iterables) | terminating | flatten one level | chain([1,2],[3]) → 1, 2, 3 |
chain.from_iterable(it) | terminating | flatten an iterable of iterables | chain.from_iterable([[1,2],[3]]) |
compress(data, sel) | terminating | filter by truthy selectors | compress('ABCD',[1,0,1,0]) → A, C |
dropwhile(pred, it) | terminating | drop while true, then yield rest | dropwhile(lambda x: x<3, [1,2,3,4]) → 3, 4 |
takewhile(pred, it) | terminating | yield while true | takewhile(lambda x: x<3, [1,2,3,4]) → 1, 2 |
filterfalse(pred, it) | terminating | inverse of filter | filterfalse(bool, [0,1,2]) → 0 |
islice(it, stop) or islice(it, start, stop, step) | terminating | lazy slice | islice(count(), 5) → 0…4 |
tee(it, n) | terminating | n independent iterators | tee(iter([1,2,3]), 2) |
groupby(it, key) | terminating | consecutive runs | see below |
accumulate(it, func) | terminating | running totals | accumulate([1,2,3]) → 1, 3, 6 |
pairwise(it) (3.10+) | terminating | sliding window of 2 | pairwise('ABCD') → AB, BC, CD |
batched(it, n) (3.12+) | terminating | tuples of size n | batched('ABCDE', 2) → AB, CD, E |
starmap(func, it) | terminating | func(*args) per tuple | starmap(pow, [(2,3),(3,2)]) → 8, 9 |
zip_longest(*its, fillvalue=…) | terminating | zip, padding shorter | zip_longest('AB','xyz', fillvalue='-') |
product(*iters, repeat=n) | combinatorial | cartesian product | product('AB', repeat=2) → 4 pairs |
permutations(it, r) | combinatorial | r-length orderings | permutations('ABC', 2) → 6 |
combinations(it, r) | combinatorial | r-length unordered | combinations('ABC', 2) → 3 |
combinations_with_replacement(it, r) | combinatorial | with 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.
from itertools import chain
list(chain([1, 2], [3, 4], [5]))
list(chain.from_iterable([[1, 2], [3, 4], [5]]))
Output:
[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.
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:
['a', 'b', 'c']
Use
chain.from_iterableinstead ofsum(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.
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:
eng ['Alice', 'Bob']
sales ['Carol']
eng ['Dan']
Note eng appears twice — the runs were not contiguous. Sort first to get the SQL behaviour.
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:
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 withlist()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.
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:
[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.
from itertools import pairwise, batched
list(pairwise('ABCDE')) # sliding window of 2
list(batched('ABCDEFG', 3)) # chunks of 3 — last may be short
Output:
[('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.
from itertools import pairwise
prices = [100, 102, 99, 104, 110]
deltas = [b - a for a, b in pairwise(prices)]
print(deltas)
Output:
[2, -3, 5, 6]
If you are stuck on Python 3.9 or earlier, polyfill
batchedwithdef 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.
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:
[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:
from itertools import islice
with open('/etc/services') as f:
for line in islice(f, 10):
print(line.rstrip())
Output:
# /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.
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:
[('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:
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:
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.
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:
([1, 2, 3, 4], [1, 2, 3, 4])
[100, 105, 110, 115, 120]
['A', 'B', 'A', 'B', 'A']
['x', 'x', 'x']
teecaches values internally. If one branch advances far ahead of the other, memory grows. Don'tteean 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.
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:
[1, 4]
[6, 4, 1, 9, 2]
A typical use: skip a file's header comment lines.
from itertools import dropwhile
lines = ['# header', '# more header', 'data1', 'data2']
for line in dropwhile(lambda l: l.startswith('#'), lines):
print(line)
Output:
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.
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:
[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.
| Name | Category | Purpose |
|---|---|---|
lru_cache(maxsize=128, typed=False) | caching | memoize last N calls (LRU eviction) |
cache (3.9+) | caching | unbounded memoize — lru_cache(maxsize=None) shortcut |
cached_property (3.8+) | caching | compute once per instance, store on __dict__ |
partial(func, *args, **kwargs) | composition | freeze leading args |
partialmethod | composition | partial for methods |
reduce(func, iterable, initial) | composition | left fold |
wraps(wrapped) | decoration | preserve metadata in decorators |
update_wrapper(wrapper, wrapped) | decoration | the function wraps decorates with |
singledispatch | dispatch | single-arg type-based function dispatch |
singledispatchmethod (3.8+) | dispatch | same, for methods |
total_ordering | dispatch | fill in __lt__/__le__/__gt__/__ge__ from one comparison |
cmp_to_key | adapter | adapt 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.
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:
354224848179261915075
CacheInfo(hits=98, misses=101, maxsize=1024, currsize=101)
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:
[2, 3, 5, 7, 11, 13, 17, 19, 23, 29]
Arguments must be hashable — lists, dicts, and sets will raise
TypeError. Convert totupleorfrozensetfirst, or write a custom key function with a wrapper.
Use
typed=Trueiff(3)andf(3.0)should cache separately — by default they share a slot because3 == 3.0andhash(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.
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:
60
60
cached_propertyrequires 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.
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:
49
27
[255, 10, 16]
partial is the cleanest way to bind a callback's extra arguments without lambdas.
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:
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.
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:
15
120
{'a': 1, 'b': 2, 'c': 3}
Since Python 3.8 you can use
math.prod()instead ofreduce(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.
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:
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.
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:
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.
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:
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.
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:
[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.
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:
[['a', 'b'], ['c', 'd'], ['e', 'f']]
Counter-style group counts with groupby + reduce
Count occurrences in a sorted stream without a collections.Counter.
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:
{'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.
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:
21
Common pitfalls
- Iterators are single-use — once you iterate an itertools result, it's empty. Wrap with
list(...)if you need to iterate twice, or usetee. groupbyrequires sorted input for SQL-style grouping — callsorted(data, key=keyfunc)first or you'll get one group per run.lru_cachearguments must be hashable —f([1,2,3])raisesTypeError. Convert mutable args to tuples.lru_cacheon methods cachesself— every distinct instance gets its own cache entry, and instances are never garbage-collected while cached. Usecached_propertyfor per-instance caching instead.cached_propertycollides with__slots__— add'__dict__'to slots or uselru_cacheon a method.partial(func, kwarg=...)cannot be overridden positionally —square = partial(pow, exp=2); square(3, 4)raisesTypeError: pow() got multiple values for argument 'exp'.- Infinite iterators with no upper bound —
for x in count():runs forever. Always bracket withisliceortakewhile. teeand memory — if one branch outpaces the other by a lot, the cache grows without bound.reduceover an empty iterable — raisesTypeErrorunless aninitialvalue is supplied. Always passinitialwhen the iterable might be empty.- Decorator order matters —
@lru_cacheabove@staticmethodworks; the reverse silently breaks becausestaticmethodreturns 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.
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:
food 30
travel 125
food 5
Retry decorator built with wraps + partial
Compose a @retry(times=3) decorator that preserves metadata and accepts arguments.
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:
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.
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:
(1, 1)
(2, 3)
CacheInfo(hits=0, misses=2, maxsize=None, currsize=2)
Cartesian-product grid search
Use product to iterate every combination of hyperparameters.
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:
best: ((0.001, 0.9, 32), 1899.968)
Single-dispatch JSON encoder
Convert mixed-type values to JSON-safe forms via singledispatch.
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:
{"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.
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:
{'name': ['alice'], 'tag': ['a', 'b']}
['file1', 'file2']