Skip to main content

FlpIt

Fluent LINQ for Python — LINQ semantics, Python-native execution

FlpIt brings a strongly typed, LINQ-style API to standard Python iterables. It combines the fluent query model and semantics of .NET LINQ with Python's native iterable and generator model.

Semantic goal: flp strives to faithfully reproduce .NET LINQ semantics.
Where Python's language or type system requires a different API contract, the contract is adapted accordingly. The implementation is actively developed, and semantic deviations may still exist.

⚡ Key Features

  • ⚡ Lazy Evaluation (FlpIt): Deferred, streaming execution where the underlying operation permits it. FlpIt wraps a standard Python Iterable[T].
  • 📦 Materialized Container (FlpList): Eager, mutable container backed by collections.UserList.
  • 🎯 LINQ Semantics: Operators such as .any(), .first(), .single(), .order_by(), and .then_by() follow .NET LINQ semantics.
  • 🔀 Independent Query Pipelines: Derived queries are independent and never mutate or retroactively affect separately created queries.
  • 🧠 Lazy Internal Materialization: Some operators, such as ordering and grouping, require internal materialization during deferred execution, while the query itself remains lazy.
  • 🔄 Source Enumeration Semantics — FlpIt preserves the enumeration characteristics of its source. A query over a re-enumerable source can be enumerated repeatedly; a one-shot source such as a Python generator remains one-shot. FlpIt does not clone or rewind arbitrary Python iterators.
  • 🔹 Static-First Typing: Designed for precise pyrefly inference without plugins (mypy and pyright might give varying results).
  • 🐍 Python-Native: Built on standard Python iterables, iterators, generators, and callable semantics.

Scope & Intent

FlpIt focuses exclusively on replicating the IEnumerable (System.Linq.Enumerable) model.

  • Direct Execution: It operates directly on standard Python callables (lambdas and functions) over Python iterables.
  • No Provider Abstraction: It does not model or emit Expression Trees (IQueryable) for translation by external providers. There is no query-provider layer that rewrites FlpIt operations into database queries or other backend-specific operations.

FlpIt therefore follows the execution model of in-memory LINQ: query operators build a pipeline, execution is deferred where the operator permits it, and each operator's actual enumeration and buffering behavior is part of the query semantics.

🤸 Flipping the Pipeline

Native Python functional operations tend to produce an inside-out, right-to-left reading pattern...

# 🔁 Native Built-ins
result = list(map(lambda x: x * 10, filter(lambda x: x % 2 == 0, range(1, 11))))

# 🤸 FlpIt Pipeline
result = (
    flp.it(range(1, 11))
    .where(lambda x: x % 2 == 0)
    .select(lambda x: x * 10)
    .to_list()
)

💡 Quick Start

from flpit import flp, FlpIt, FlpList

# Deferred iterable via shorthand
data: FlpIt[int] = flp.it(range(1, 11))

# Lazy, streaming pipeline
query: FlpIt[int] = (
    data
    .where(lambda x: x % 2 == 0)
    .select(lambda x: x * 10)
)

# Explicit materialization
result: FlpList[int] = query.to_list()
# [20, 40, 60, 80, 100]

# Or start directly with an eager container
eager_list: FlpList[int] = flp.lst([1, 2, 3, 4])

Typed Domain Objects

from dataclasses import dataclass
from flpit import flp


@dataclass
class Employee:
    name: str
    role: str
    salary: int


@dataclass
class Department:
    name: str
    employees: list[Employee]


company = [
    Department(
        "Engineering",
        [
            Employee("Alice", "Backend Engineer", 95_000),
            Employee("Bob", "Frontend Engineer", 90_000),
        ],
    ),
    Department(
        "Design",
        [
            Employee("Charlie", "UI Designer", 82_000),
        ],
    ),
]

# Extract the names of backend engineers earning at least 90,000.
backend_engineers = (
    flp.it(company)
    .select_many(lambda dept: dept.employees)
    .where(lambda emp: emp.role == "Backend Engineer")
    .where(lambda emp: emp.salary >= 90_000)
    .select(lambda emp: emp.name)
)

# The query is evaluated when it is enumerated.
print(list(backend_engineers))
# ['Alice']

🔬 LINQ Semantics, Python Runtime

flpit deliberately separates the two:

  • LINQ defines the query semantics.
  • Python provides the underlying iterable and execution model.

For example:

query = flp.range(1, 11).where(lambda x: x % 2 == 0)

query.any()       # LINQ-style terminal operation
query.first()     # LINQ-style terminal operation
query.single()    # LINQ-style terminal operation

The query can also be consumed through normal Python built-ins:

any(query)
list(query)

The two forms serve different purposes: LINQ-style operators provide the query API and its associated semantics, while Python built-ins continue to operate through the normal iterable protocol.

Source Repeatability

FlpIt does not imply that every source is repeatable. Re-enumeration depends on the underlying source:

source = [1, 2, 3]
query = flp.it(source)

list(query)  # [1, 2, 3]
list(query)  # [1, 2, 3]

A one-shot iterator remains one-shot:

source = (x for x in range(3))
query = flp.it(source)

list(query)  # [0, 1, 2]
list(query)  # []

Intentional Deviation from Python List Contracts (.count())

To faithfully provide standard .NET LINQ semantics, FlpList deliberately uses LINQ-style count() semantics instead of Python list.count(value) semantics of Python's standard list.count(value).

  • Vanilla Python list.count(value) counts occurrences of a specific value.
  • FlpIt FlpList.count() counts all elements.
  • FlpIt FlpList.count(predicate) counts elements matching the predicate.
# Pure LINQ semantics instead of standard list behavior
total_evens = eager_list.count(lambda x: x % 2 == 0)

Because FlpList is an explicit, fluent wrapper designed to enforce LINQ behavior, this signature change is a deliberate design choice. If you need the native Python behavior to count occurrences of a specific item value, use the explicitly provided fallback method:

# Use the fallback for traditional Python value-counting
occurrences = eager_list.count_item(4)

🔗 Independent Query Pipelines

Query pipelines can be composed independently without mutating the queries they were derived from:

For example:

@dataclass
class Item: 
    group: str 
    value: int

original = flp.it([Item("A", 2), Item("B", 1)])

ordered = original.order_by(lambda x: x.group)

extended = original.concat([Item("A", 1), Item("B", 2)])

ordered_by_group_and_value = ordered.then_by(lambda x: x.value)

The resulting query graph is:

original
   │
   ├── order_by ──→ ordered
   │                  │
   │                  └── then_by ──→ ordered_by_group_and_value
   │
   └── concat ─────→ extended

ordered_by_group_and_value is derived from ordered.
It does not modify original, and it is not affected by the separately created extended query.

This matches the independent-query behavior of .NET LINQ.

Execution Characteristics

Lazy execution does not mean that every operator is streaming. Some operators must consume and buffer their input before they can produce their result. Sorting requires buffering: Any exact sort of an arbitrary input must inspect its elements before it can produce the ordered result. This is not specific to FlpIt; it is an inherent characteristic of sorting and applies equally to native Python sorting and .NET LINQ OrderBy().

Operator Execution deferred Processes incrementally Buffers input
where() / select() Yes Yes No
order_by() Yes No Yes
group_by() Yes No Yes

order_by() and group_by() therefore remain deferred at query construction time, but enumeration of either operator requires buffering its upstream input.

group_by() produces a query of groupings rather than terminating the pipeline:

groups = query.group_by(lambda x: x % 3)

result = (
    groups
    .where(...)
    .select(...)
    .to_list()
)

Terminal operators instead evaluate the query and return a result rather than another query:

query.any()
query.first()
query.single()
query.count()
query.aggregate(...)
query.to_list()

For example, aggregate() processes its source incrementally but must consume the source completely to produce its final accumulated value.

🧠 Lazy Pipelines

Streaming operators do not materialize their upstream input:

query = (
    flp.it(source)
    .where(predicate)
    .select(selector)
    .order_by(key_selector)
    .take(100)
)

Conceptually:

source
  │
  ▼
where       ← streaming
  │
  ▼
select      ← streaming
  │
  ▼
order_by    ← lazy, internally buffers when enumerated
  │
  ▼
take        ← streaming
  │
  ▼
to_list     ← terminal materialization

Operations such as order_by() necessarily materialize their input internally to establish ordering.

⚙️ Installation

uv add flpit

📜 License

Distributed under the MIT License. See LICENSE for more information.

Metadata

Release files for flpit 0.1.6

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

Source distribution (sdist)

Source distribution for flpit 0.1.6
File Size Uploaded Provenance
flpit-0.1.6.tar.gz 12.6 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for flpit 0.1.6
File Interpreter ABI Platform
flpit-0.1.6-py3-none-any.whl Python 3 none any Details

Total release size: 26.5 kB

Release files / flpit-0.1.6.tar.gz

Download URL flpit-0.1.6.tar.gz
Size 12.6 kB
Tags Source
SHA-256 checksum
How to use checksums
7731f067ff6e8ddd32c6dd892705964e6b0db0915992d0d847d9fe052e748ab6
BLAKE2b-256 checksum
How to use checksums
51dadd028ef9ec38335710f369c5a4c35e044fa95623e179ee7662a5be05db82
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.10 {"installer":{"name":"uv","version":"0.12.10","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release files / flpit-0.1.6-py3-none-any.whl

Download URL flpit-0.1.6-py3-none-any.whl
Size 13.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
d75a2f781289a209267bc79d96ea2c8a08085f1b0922feaeecc86bc33d322efb
BLAKE2b-256 checksum
How to use checksums
109ef9cbd8fa0bca5d6113a0d51d706641272dd522fb383f25d0ae1f7ec39600
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.10 {"installer":{"name":"uv","version":"0.12.10","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Security for flpit 0.1.6

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

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