Comprehensions¶
Comprehensions provide concise syntax for creating collections by transforming and filtering iterables.
List Comprehensions¶
# Basic transformation
squares = [x ** 2 for x in range(10)]
# [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
# With filter condition
evens = [x for x in range(10) if x % 2 == 0]
# [0, 2, 4, 6, 8]
# Transformation and filter
doubled_evens = [x * 2 for x in range(10) if x % 2 == 0]
# [0, 4, 8, 12, 16]
# Nested comprehension (comprehension inside comprehension)
matrix = [[i * j for j in range(3)] for i in range(3)]
# [[0, 0, 0], [0, 1, 2], [0, 2, 4]]
Note: Both multiple
forclauses in a single comprehension (e.g.,[(x, y) for x in A for y in B]) and nested comprehensions where the element expression is itself a comprehension (e.g.,[[expr for ...] for ...]) are supported — see Multiple For Clauses below.
Implementation
- 🔄 Lowered - LINQ expressions:
- [expr for x in iter] → .Select(x => expr).ToList()
- [expr for x in iter if cond] → .Where(x => cond).Select(x => expr).ToList()
Filter and Transform Order:
The filter (if clause) is applied before the transformation, matching Python semantics exactly:
[x * 2 for x in items if x > 0]
# Equivalent to:
# result = []
# for x in items:
# if x > 0: # Filter first
# result.append(x * 2) # Then transform
This maps to LINQ's .Where(...).Select(...) ordering:
Multiple For Clauses¶
Comprehensions can have multiple for clauses, which are evaluated left-to-right like nested loops:
# Multiple for clauses
pairs = [(x, y) for x in range(3) for y in range(3)]
# Equivalent to:
# result = []
# for x in range(3):
# for y in range(3):
# result.append((x, y))
# [(0,0), (0,1), (0,2), (1,0), (1,1), (1,2), (2,0), (2,1), (2,2)]
# With filter on inner loop
pairs_filtered = [(x, y) for x in range(3) for y in range(3) if x != y]
# [(0,1), (0,2), (1,0), (1,2), (2,0), (2,1)]
# Later clauses can reference earlier variables
triangular = [(x, y) for x in range(4) for y in range(x)]
# [(1,0), (2,0), (2,1), (3,0), (3,1), (3,2)]
Implementation
- 🔄 Lowered - LINQ SelectMany:
// [(x, y) for x in range(3) for y in range(3)]
Enumerable.Range(0, 3)
.SelectMany(x => Enumerable.Range(0, 3), (x, y) => (x, y))
.ToList();
Dict Comprehensions¶
# Basic dict comprehension
square_dict = {x: x ** 2 for x in range(5)}
# {0: 0, 1: 1, 2: 4, 3: 9, 4: 16}
# From existing collection
names = ["alice", "bob", "charlie"]
name_lengths = {name: len(name) for name in names}
# {"alice": 5, "bob": 3, "charlie": 7}
# With filter
long_names = {name: len(name) for name in names if len(name) > 3}
# {"alice": 5, "charlie": 7}
Implementation
- 🔄 Lowered - .ToDictionary(x => key, x => value)
Set Comprehensions¶
# Basic set comprehension
unique_lengths = {len(word) for word in ["apple", "banana", "cherry"]}
# {5, 6}
# With filter
short_lengths = {len(word) for word in ["apple", "banana", "cherry"] if len(word) < 7}
# {5, 6}
Implementation
- 🔄 Lowered - .Select(...).ToHashSet()
Comprehension Variable Scoping¶
Variables declared in comprehensions are scoped to that comprehension and do not leak into the enclosing scope:
# Variables don't leak
squares = [i ** 2 for i in range(10)]
print(i) # ERROR: 'i' does not exist in this scope
# Dict comprehension variables don't leak
ages = {name: age for name, age in pairs}
print(name) # ERROR: 'name' does not exist in this scope
Walrus Operator in Comprehensions:
Variables assigned using the walrus operator (:=) inside a comprehension are also comprehension-local. They do not leak to the containing scope:
# Walrus useful within comprehension to avoid recomputation
results = [y * 2 for x in items if (y := expensive(x)) > 0]
# y is local to the comprehension - used in both filter and transform
print(y) # ERROR: 'y' does not exist in this scope
Note: This differs from Python 3.8+, where walrus assignments leak. In Sharpy, the syntactic boundary ([...] / {...}) is the semantic boundary: nothing leaks. See walrus_operator.md for more details.
Shadowing Outer Variables:
Comprehension variables may shadow variables from the enclosing scope. The outer variable is not modified:
x = 100
squares = [x ** 2 for x in range(5)] # This 'x' shadows outer 'x'
print(x) # 100 - outer 'x' unchanged
print(squares) # [0, 1, 4, 9, 16]
name = "outer"
lengths = {name: len(name) for name in ["a", "bb", "ccc"]}
print(name) # "outer" - unchanged
Unique Variable Names Required:
Within a single comprehension, each for clause must use a unique variable name. Reusing a variable name across multiple for clauses is a compile-time error:
# ✅ OK - different variable names in each for clause
pairs = [(x, y) for x in range(3) for y in range(3)]
# ✅ OK - shadows outer scope (different from reuse within comprehension)
x = 100
result = [(x, y) for x in range(3) for y in range(3)]
# ❌ ERROR - same variable name in multiple for clauses
bad = [x for x in range(3) for x in range(3)]
# Compile error: Variable 'x' already declared in this comprehension
# ❌ ERROR - even with different structure
also_bad = [(x, x) for x in range(3) for x in range(3)]
# Compile error: Variable 'x' already declared in this comprehension
Rationale: Allowing the same variable name in multiple for clauses creates confusing code where the inner loop shadows the outer loop variable. This is almost always a bug rather than intentional behavior. Sharpy prohibits this pattern at compile time.
Filter Clause Scope:
Filter conditions (if clauses) can reference any variable declared in preceding for clauses:
# Filter can use variables from any preceding for clause
result = [(x, y) for x in range(5) for y in range(5) if x + y < 4]
# [(0,0), (0,1), (0,2), (0,3), (1,0), (1,1), (1,2), (2,0), (2,1), (3,0)]
# Multiple filters
filtered = [x for x in range(20) if x % 2 == 0 if x % 3 == 0]
# [0, 6, 12, 18]
Async Comprehensions¶
Async comprehensions (async for inside comprehensions and await inside comprehensions) are intentionally not supported in Sharpy. This is a deliberate design decision — see async_programming.md for rationale and workarounds.