Examples¶
These are the runnable scripts in examples/, from simple to complex. CI runs every one of them.
01 Basics¶
"""Basics: filter, project and pick elements from a plain list of pydantic models."""
from pydantic import BaseModel
import pydantic_linq # noqa: F401 (adds the LINQ operators to list / tuple / set)
from pydantic_linq import Enumerable
class Product(BaseModel):
name: str
price: float
stock: int
products = [
Product(name="Keyboard", price=49.0, stock=12),
Product(name="Mouse", price=19.5, stock=0),
Product(name="Monitor", price=219.0, stock=4),
Product(name="Cable", price=5.0, stock=120),
]
# Where + Select, called straight on the list
in_stock = products.where(lambda p: p.stock > 0).select(lambda p: p.name).to_list()
print("in stock:", in_stock)
# Element operators
print("first over $100:", products.first(lambda p: p.price > 100).name)
print("first over $1000:", products.first_or_default(lambda p: p.price > 1000))
# Quantifiers and counting. list.count(x) keeps its Python meaning, so wrap for the LINQ count().
print("any sold out:", products.any(lambda p: p.stock == 0))
print("sold out count:", Enumerable(products).count(lambda p: p.stock == 0))
# Queries are deferred: nothing runs until you enumerate
cheap = products.where(lambda p: p.price < 20)
products.append(Product(name="Sticker", price=1.0, stock=500))
print("cheap (evaluated now):", cheap.select(lambda p: p.name).to_list())
02 Sorting and paging¶
"""Sorting and paging: OrderBy / ThenBy, Skip / Take, Chunk."""
from pydantic import BaseModel
import pydantic_linq # noqa: F401
class Employee(BaseModel):
name: str
dept: str
salary: int
staff = [
Employee(name="Ava", dept="Eng", salary=120),
Employee(name="Ben", dept="Ops", salary=90),
Employee(name="Cleo", dept="Eng", salary=120),
Employee(name="Dan", dept="Eng", salary=105),
Employee(name="Eve", dept="Ops", salary=95),
]
# Multi-key, stable sort: dept ascending, then salary descending, then name
ordered = staff.order_by(lambda e: e.dept).then_by_descending(lambda e: e.salary).then_by(lambda e: e.name)
print([f"{e.dept}:{e.name}:{e.salary}" for e in ordered])
# Paging
page_size = 2
for page in range(3):
names = ordered.skip(page * page_size).take(page_size).select(lambda e: e.name).to_list()
print(f"page {page + 1}:", names)
# Chunk does the same in one call
print("chunks:", ordered.select(lambda e: e.name).chunk(2).to_list())
# Take with a range: slice(1, -1) is C#'s Take(1..^1)
print("without top and bottom:", ordered.take(slice(1, -1)).select(lambda e: e.name).to_list())
03 Grouping and aggregation¶
"""Grouping and aggregation: GroupBy, ToLookup, CountBy, AggregateBy, Sum / Average / MaxBy."""
from datetime import date
from pydantic import BaseModel
import pydantic_linq # noqa: F401
class Sale(BaseModel):
region: str
rep: str
amount: float
day: date
sales = [
Sale(region="North", rep="Ann", amount=120.0, day=date(2026, 1, 3)),
Sale(region="North", rep="Bob", amount=80.0, day=date(2026, 1, 4)),
Sale(region="South", rep="Cid", amount=200.0, day=date(2026, 1, 4)),
Sale(region="North", rep="Ann", amount=60.0, day=date(2026, 2, 1)),
Sale(region="South", rep="Dee", amount=40.0, day=date(2026, 2, 2)),
]
# GroupBy with a result selector: one summary row per region
summary = sales.group_by(
lambda s: s.region,
result_selector=lambda region, items: (region, items.count(), items.sum(lambda s: s.amount)),
).to_list()
print("region, orders, total:", summary)
# Each Grouping is itself an Enumerable with a .key
for g in sales.group_by(lambda s: s.day.month):
print(f"month {g.key}: avg {g.average(lambda s: s.amount):.2f}, reps {g.select(lambda s: s.rep).distinct().to_list()}")
# A Lookup is an indexable group set; a missing key gives an empty sequence
by_rep = sales.to_lookup(lambda s: s.rep, lambda s: s.amount)
print("Ann:", by_rep["Ann"].to_list(), "Zed:", by_rep["Zed"].to_list())
# .NET 9 CountBy / AggregateBy
print("orders per rep:", sales.count_by(lambda s: s.rep).to_list())
print("revenue per rep:", sales.aggregate_by(lambda s: s.rep, 0.0, lambda acc, s: acc + s.amount).to_list())
# Scalar aggregates
print("total:", sales.sum(lambda s: s.amount))
print("biggest sale:", sales.max_by(lambda s: s.amount).rep)
print("running product:", [1, 2, 3, 4].aggregate(lambda a, b: a * b))
04 Joins and sets¶
"""Joins and set operations across two model lists."""
from pydantic import BaseModel
import pydantic_linq # noqa: F401
class Customer(BaseModel):
id: int
name: str
class Order(BaseModel):
customer_id: int
item: str
customers = [Customer(id=1, name="Ann"), Customer(id=2, name="Bob"), Customer(id=3, name="Cid")]
orders = [
Order(customer_id=1, item="book"),
Order(customer_id=1, item="pen"),
Order(customer_id=3, item="lamp"),
Order(customer_id=9, item="ghost"),
]
# Inner join
print("join:", customers.join(orders, lambda c: c.id, lambda o: o.customer_id, lambda c, o: (c.name, o.item)).to_list())
# GroupJoin: every customer with their orders (possibly none)
print(
"group join:",
customers.group_join(
orders, lambda c: c.id, lambda o: o.customer_id, lambda c, os: (c.name, os.select(lambda o: o.item).to_list())
).to_list(),
)
# .NET 10 LeftJoin / RightJoin: the missing side is None
print(
"left join:",
customers.left_join(
orders, lambda c: c.id, lambda o: o.customer_id, lambda c, o: (c.name, o.item if o else None)
).to_list(),
)
print(
"right join:",
customers.right_join(
orders, lambda c: c.id, lambda o: o.customer_id, lambda c, o: (c.name if c else None, o.item)
).to_list(),
)
# Set operations use value equality, so equal (non-frozen) models count as duplicates
batch_a = [Customer(id=1, name="Ann"), Customer(id=2, name="Bob")]
batch_b = [Customer(id=2, name="Bob"), Customer(id=4, name="Dee")]
print("union:", batch_a.union(batch_b).select(lambda c: c.name).to_list())
print("intersect:", batch_a.intersect(batch_b).select(lambda c: c.name).to_list())
print("except:", batch_a.except_(batch_b).select(lambda c: c.name).to_list())
# *By variants compare by a key; the second sequence holds keys
print("customers without orders:", customers.except_by(orders.select(lambda o: o.customer_id), lambda c: c.id)
.select(lambda c: c.name).to_list())
05 Pydantic integration¶
"""Pydantic integration: validate raw data with cast(), use Enumerable[Model] as a field, round-trip JSON."""
from pydantic import BaseModel, ValidationError
import pydantic_linq # noqa: F401
from pydantic_linq import Enumerable
class LineItem(BaseModel):
sku: str
qty: int
unit_price: float
class Invoice(BaseModel):
number: str
lines: Enumerable[LineItem] # validated like list[LineItem]; behaves like LINQ
@property
def total(self) -> float:
return self.lines.sum(lambda line: line.qty * line.unit_price)
# Raw rows, e.g. from an API or CSV: strings get coerced, dicts become models
raw = [
{"sku": "A-1", "qty": "2", "unit_price": "9.99"},
{"sku": "B-7", "qty": 1, "unit_price": 120},
]
items = raw.cast(LineItem).to_list()
print("cast:", items)
# strict=True disables coercion
try:
raw.cast(LineItem, strict=True).to_list()
except ValidationError as e:
print("strict cast failed:", e.error_count(), "errors")
invoice = Invoice(number="INV-1", lines=items)
print("total:", round(invoice.total, 2))
print("big lines:", invoice.lines.where(lambda line: line.unit_price > 50).select(lambda line: line.sku).to_list())
# Serializes as a plain list; validates back into an Enumerable
payload = invoice.model_dump_json()
print("json:", payload)
again = Invoice.model_validate_json(payload)
print("round-trip type:", type(again.lines).__name__, "count:", again.lines.count())
# Value equality: non-frozen models are unhashable, distinct() still works
dupes = [LineItem(sku="A-1", qty=1, unit_price=1.0)] * 3
print("distinct:", dupes.distinct().count())
# of_type filters by class, like OfType<T>
mixed = [items[0], "noise", 42, items[1]]
print("of_type:", mixed.of_type(LineItem).select(lambda line: line.sku).to_list())
06 Async¶
"""Async: AsyncEnumerable over async sources, with sync or async selectors."""
import asyncio
from pydantic import BaseModel
import pydantic_linq # noqa: F401
from pydantic_linq import AsyncEnumerable
class User(BaseModel):
id: int
name: str
class Profile(BaseModel):
user_id: int
followers: int
async def fetch_users():
"""An async generator source, e.g. a paginated API or a DB cursor."""
for i, name in enumerate(["ann", "bob", "cid", "dee"], start=1):
await asyncio.sleep(0.01)
yield User(id=i, name=name)
async def fetch_profile(user: User) -> Profile:
"""An async selector, the System.Linq.Async SelectAwait overload."""
await asyncio.sleep(0.01)
return Profile(user_id=user.id, followers=user.id * 150)
async def is_active(user: User) -> bool:
await asyncio.sleep(0)
return user.id != 2
async def main() -> None:
users = AsyncEnumerable(fetch_users())
# Async predicate + sync projection
names = await users.where(is_active).select(lambda u: u.name.title()).to_list()
print("active:", names)
# Async generator sources are one-shot (as in Python), so re-wrap for a second query
users = AsyncEnumerable(fetch_users())
popular = await (
users.select(fetch_profile)
.where(lambda p: p.followers > 200)
.order_by_descending(lambda p: p.followers)
.select(lambda p: p.user_id)
.to_list()
)
print("popular ids:", popular)
# Sync lists enter async with to_async_enumerable(); terminal operators are awaited
local = [User(id=10, name="eve"), User(id=11, name="fay")]
print("total followers:", await local.to_async_enumerable().select(fetch_profile).sum(lambda p: p.followers))
print("first:", (await local.to_async_enumerable().first(lambda u: u.id > 10)).name)
# Grouping yields AsyncGrouping, an AsyncEnumerable with a .key
async for g in AsyncEnumerable(fetch_users()).group_by(lambda u: u.id % 2 == 0):
print("even" if g.key else "odd", await g.select(lambda u: u.name).to_list())
asyncio.run(main())
07 End-to-end sales report¶
"""End to end: raw JSON in, validated models, async enrichment, joins, grouping, a typed report out."""
import asyncio
import json
from datetime import date
from pydantic import BaseModel
from pydantic_linq import Enumerable
RAW_ORDERS = json.loads("""[
{"id": 1, "customer": "acme", "sku": "W-1", "qty": 3, "day": "2026-03-01"},
{"id": 2, "customer": "globex", "sku": "G-9", "qty": 1, "day": "2026-03-01"},
{"id": 3, "customer": "acme", "sku": "G-9", "qty": 2, "day": "2026-03-02"},
{"id": 4, "customer": "initech","sku": "W-1", "qty": 10, "day": "2026-03-03"},
{"id": 5, "customer": "acme", "sku": "X-0", "qty": 1, "day": "2026-03-03"},
{"id": 2, "customer": "globex", "sku": "G-9", "qty": 1, "day": "2026-03-01"}
]""")
class Order(BaseModel):
id: int
customer: str
sku: str
qty: int
day: date
class Product(BaseModel):
sku: str
name: str
price: float
class CustomerRow(BaseModel):
customer: str
orders: int
revenue: float
top_product: str
class Report(BaseModel):
rows: Enumerable[CustomerRow]
unknown_skus: list[str]
best_day: date
CATALOG = [Product(sku="W-1", name="Widget", price=4.5), Product(sku="G-9", name="Gadget", price=30.0)]
async def price_lookup(sku: str) -> float | None:
"""Pretend pricing service; async so it shows the async layer."""
await asyncio.sleep(0.01)
return {"W-1": 4.0, "G-9": 28.0}.get(sku) # live price, may differ from the catalog
async def build_report() -> Report:
# 1. Validate the raw JSON and drop duplicate records (value equality on models)
orders = Enumerable(RAW_ORDERS).cast(Order).distinct().to_list()
# 2. SKUs with no catalog entry: except_by against the catalog keys
unknown = orders.except_by(CATALOG.select(lambda p: p.sku), lambda o: o.sku).select(lambda o: o.sku).to_list()
# 3. Join to the catalog; live price from the async service, catalog price as fallback
async def priced(pair: tuple[Order, Product]) -> tuple[Order, Product, float]:
order, product = pair
live = await price_lookup(product.sku)
return order, product, live if live is not None else product.price
lines = await (
orders.join(CATALOG, lambda o: o.sku, lambda p: p.sku, lambda o, p: (o, p))
.to_async_enumerable()
.select(priced)
.to_list()
)
# 4. Per-customer rows, highest revenue first
rows = (
lines.group_by(lambda line: line[0].customer)
.select(
lambda g: CustomerRow(
customer=g.key,
orders=g.count(),
revenue=g.sum(lambda line: line[0].qty * line[2]),
top_product=g.max_by(lambda line: line[0].qty * line[2])[1].name,
)
)
.order_by_descending(lambda r: r.revenue)
.then_by(lambda r: r.customer)
.to_list()
)
# 5. Day with the most units shipped
best_day = orders.aggregate_by(lambda o: o.day, 0, lambda n, o: n + o.qty).max_by(lambda kv: kv[1])[0]
return Report(rows=rows, unknown_skus=unknown, best_day=best_day)
report = asyncio.run(build_report())
print(report.model_dump_json(indent=2))
08 Entity Framework (SQLModel)¶
"""Entity Framework style queries over SQLModel: LINQ that runs as SQL.
Needs the extra: pip install pydantic-linq[sqlmodel]
"""
from sqlmodel import Field, Relationship, SQLModel, col, create_engine
from pydantic_linq.orm import DbContext
class Blog(SQLModel, table=True):
id: int | None = Field(default=None, primary_key=True)
name: str
posts: list["Post"] = Relationship(back_populates="blog")
class Post(SQLModel, table=True):
id: int | None = Field(default=None, primary_key=True)
title: str
likes: int
blog_id: int | None = Field(default=None, foreign_key="blog.id")
blog: Blog | None = Relationship(back_populates="posts")
with DbContext(create_engine("sqlite://")) as db:
db.ensure_created()
# Add / SaveChanges
python, dotnet = Blog(name="Python"), Blog(name=".NET")
db.add_range(
[
Post(title="Generics in 3.12", likes=120, blog=python),
Post(title="Pattern matching", likes=80, blog=python),
Post(title="LINQ deep dive", likes=300, blog=dotnet),
Post(title="EF Core 10", likes=210, blog=dotnet),
Post(title="Records", likes=40, blog=dotnet),
]
)
print("saved:", db.save_changes())
posts = db.set(Post) # DbSet<Post>
# Lambdas build SQL: use & | ~ for and / or / not.
popular = posts.where(lambda p: (p.likes > 100) & ~col(p.title).contains("3.12")).order_by_descending(lambda p: p.likes)
print(popular.to_query_string())
print("popular:", popular.select(lambda p: p.title).to_list())
# Paging, element operators and aggregates are SQL too.
print("page 2:", posts.order_by(lambda p: p.id).skip(2).take(2).select(lambda p: p.title).to_list())
print("top:", posts.max_by(lambda p: p.likes).title, "| total likes:", posts.sum(lambda p: p.likes))
# GroupBy + Select -> GROUP BY / HAVING
per_blog = (
posts.group_by(lambda p: p.blog_id)
.where(lambda g: g.count() > 1)
.select(lambda g: (g.key, g.count(), g.average(lambda p: p.likes)))
)
print("per blog:", per_blog.to_list())
# Join -> INNER JOIN; the result selector picks columns.
titles = posts.join(db.set(Blog), lambda p: p.blog_id, lambda b: b.id, lambda p, b: (b.name, p.title))
print("joined:", titles.order_by(lambda t: t[1]).take(3).to_list())
# Include / ThenInclude load relationships eagerly.
for blog in db.set(Blog).include(lambda b: b.posts).order_by(lambda b: b.name):
print(blog.name, [p.title for p in blog.posts])
# A select SQL can't express runs in memory on the fetched rows (EF client projection).
print(posts.order_by(lambda p: p.likes).select(lambda p: f"{p.title} ({p.likes})").first())
# Tracked changes, ExecuteUpdate / ExecuteDelete, raw SQL.
records = posts.single(lambda p: p.title == "Records")
records.likes += 1
db.save_changes()
print("bumped:", posts.where(lambda p: p.likes < 100).execute_update(lambda p: {p.likes: p.likes + 10}))
print("deleted:", posts.where(lambda p: p.likes < 60).execute_delete())
db.save_changes()
print("raw:", db.from_sql(Post, "SELECT * FROM post WHERE likes > :n", n=200).count())
09 Entity Framework, async¶
"""Async Entity Framework style queries: an AsyncEngine and EF's *Async terminal operators.
Needs the extra and an async driver: pip install pydantic-linq[sqlmodel] aiosqlite
"""
import asyncio
from sqlalchemy.ext.asyncio import create_async_engine
from sqlmodel import Field, SQLModel
from pydantic_linq.orm import DbContext
class Order(SQLModel, table=True):
id: int | None = Field(default=None, primary_key=True)
customer: str
total: float
async def main() -> None:
engine = create_async_engine("sqlite+aiosqlite://")
async with DbContext(engine) as db:
await db.ensure_created_async()
db.add_range(
Order(customer=c, total=t)
for c, t in [("ann", 40.0), ("bob", 15.5), ("ann", 99.9), ("cid", 12.0), ("bob", 70.0)]
)
await db.save_changes_async()
orders = db.set(Order)
print("big orders:", await orders.where(lambda o: o.total > 50).count_async())
print("first:", (await orders.order_by(lambda o: o.total).first_async()).customer)
print("revenue:", await orders.sum_async(lambda o: o.total))
by_customer = orders.group_by(lambda o: o.customer).select(lambda g: (g.key, g.sum(lambda o: o.total)))
print("by customer:", await by_customer.to_dictionary_async(lambda kv: kv[0], lambda kv: kv[1]))
# AsAsyncEnumerable: continue with System.Linq.Async operators in memory.
print("chunks:", await orders.order_by(lambda o: o.id).as_async_enumerable().chunk(2).count())
async def notify(order: Order) -> None:
await asyncio.sleep(0)
print(" notify", order.customer, order.total)
await orders.where(lambda o: o.customer == "ann").for_each_async(notify)
print("deleted:", await orders.where(lambda o: o.total < 20).execute_delete_async())
await db.save_changes_async()
await engine.dispose()
asyncio.run(main())