Skip to main content

Friendly Sequences

Inspired by Scala Sequence class [1] and iterchain [2], but with a good typing support.

[1] https://alvinalexander.com/scala/seq-class-methods-examples-syntax/ [2] https://github.com/Evelyn-H/iterchain

Motivation

It is possible to compose functions in python with many functional programming primitives, like map, filter, reduce etc. But, in my opinion, looks a bit ugly and you need to get use to this structure. For example, you can write something like this:

import itertools

from functools import reduce

assert (
    reduce(
        lambda left, right: f"{left}{right}",
        map(
            str,
            sorted(
                filter(
                    lambda x: x != 2,
                    map(
                        lambda x: x + 1,
                        itertools.chain.from_iterable(
                            zip(
                                (1, 2),
                                (3, 4),
                            )
                        ),
                    ),
                )
            ),
        ),
        "",
    )
    == "345"
)

or even this:

import itertools

assert (
    "".join(
        sorted(
            str(x)
            for x in (
                x
                for x in (
                    x + 1
                    for x in itertools.chain.from_iterable(
                        zip(
                            (1, 2),
                            (3, 4),
                        )
                    )
                )
                if x != 2
            )
        )
    )
    == "345"
)

but with the friendly-sequences it is just this:

from friendly_sequences import Seq

assert (
    Seq[int]((1, 2))
    .zip(Seq[int]((3, 4)))
    .flat_map(lambda x: x + 1)
    .filter(lambda x: x != 2)
    .sort()
    .map(str)
    .fold(lambda left, right: f"{left}{right}", "")
) == "345"

Async

The moment a stage in the pipeline does I/O, the chain breaks down and you fall back to hand-rolled asyncio — which is the same nested shape all over again:

import asyncio

async def fetch_all(urls):
    semaphore = asyncio.Semaphore(20)

    async def fetch_one(url):
        async with semaphore:
            return await fetch(url)

    return [
        result
        for result in await asyncio.gather(
            *(fetch_one(url) for url in urls if is_valid(url))
        )
        if result is not None
    ]

AsyncSeq keeps it a chain:

from friendly_sequences import Seq

results = await (
    Seq(urls)
    .filter(is_valid)
    .to_async()
    .map_concurrent(fetch, limit=20)
    .filter(lambda result: result is not None)
    .to_list()
)

The two are not quite equivalent, and the difference is the point: asyncio.gather builds a task per URL up front, so a million URLs is a million tasks. map_concurrent keeps a sliding window of at most limit in-flight tasks and never materializes the input.

Every operator takes either a plain or an async callable, so you rarely have to think about which you have:

from friendly_sequences import AsyncSeq

async def double(x: int) -> int:
    return x * 2

assert await AsyncSeq((1, 2, 3)).map(double).sum() == 12
assert await AsyncSeq((1, 2, 3)).map(str).join("-") == "1-2-3"

AsyncSeq mirrors the whole Seq surface — map, flat_map, starmap, flatten, filter, take, sort, zip, and the terminals — with two differences: combinators return an AsyncSeq, and every terminal is a coroutine you await.

Concurrency operators

These have no Seq counterpart:

Operator Behaviour
map_concurrent(func, limit=10) Bounded concurrent map, results in input order
map_unordered(func, limit=10) Same window, results in completion order — better tail latency
chunked(n) Batches into tuples of up to n; a short final batch is kept
throttle(per_second) Minimum interval between yields
timeout(total=..., per_item=...) Bounds the whole stream, each item, or both

Things worth knowing

Clean up early exits with aclose(). If you abandon a pipeline before it is exhausted, call aclose() on it. Each stage closes its own upstream, so one call finalizes the whole chain and cancels any in-flight work. Consuming a pipeline to exhaustion needs no cleanup.

pipeline = AsyncSeq(urls).map_concurrent(fetch, limit=20)
first = await anext(pipeline)
await pipeline.aclose()

A firing per_item timeout can leave the source half-consumed — a coroutine cancelled mid-request. If you catch the TimeoutError and intend to carry on, aclose() the sequence.

sort and chunked are not async def, even though they buffer. They return an AsyncSeq that drains on first use, so the chain keeps reading left to right instead of becoming await (await seq.sort()).to_list().

AsyncSeq is pull-based. For backpressure-aware streaming, use a channel library.

Installation

$ pip install friendly-sequences

Requires Python 3.12 or newer.

Metadata

Release files for friendly-sequences 2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for friendly-sequences 2.0
File Size Uploaded
friendly_sequences-2.0.tar.gz 14.8 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for friendly-sequences 2.0
File Interpreter ABI Platform
friendly_sequences-2.0-py3-none-any.whl Python 3 none any Details

Total release size: 26.0 kB

Release files / friendly_sequences-2.0.tar.gz

Download URL friendly_sequences-2.0.tar.gz
Size 14.8 kB
Tags Source
SHA-256 checksum
How to use checksums
e616b9fd3a7ce1f2109eb61c3b4b4ca7c16cf96d9c6c3e7891f81f15b0850655
BLAKE2b-256 checksum
How to use checksums
2707c9b3744a72c916c0130a89b5b42839ecb596a7e5b01b6d7f2a1b0b14361c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via pdm/2.29.0 CPython/3.12.3 Linux/6.17.0-1022-azure

Release files / friendly_sequences-2.0-py3-none-any.whl

Download URL friendly_sequences-2.0-py3-none-any.whl
Size 11.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
e608e7a0fbff33c7ed7c50e03009970fbbccc8246dd6ff37a9f0b977ebca89fd
BLAKE2b-256 checksum
How to use checksums
4cd95636f6873f1306b1bba67a5727b4834184b5884f83674db1b7ffa87d45e0
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via pdm/2.29.0 CPython/3.12.3 Linux/6.17.0-1022-azure

Release history Release notifications | RSS feed

This release

2.0 This release

2 release files

1.14

2 release files

1.13

2 release files

1.12

2 release files

1.11

2 release files

1.10

2 release files

1.9

2 release files

1.8

2 release files

1.7

2 release files

1.6

2 release files

1.5

2 release files

1.4

2 release files

1.3

2 release files

1.2

2 release files

1.1

2 release files

1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page