Tuple Unpacking¶
Sharpy supports destructuring tuples and lists into individual variables across multiple contexts: assignments, for loops, and comprehensions.
Simple Tuple Unpacking¶
Unpack a tuple into variables by matching the number of elements:
The number of targets must match the number of tuple elements:
Implementation
- ✅ Native - C# tuple deconstruction: var (x, y) = point;
Redundant Parentheses Around Targets¶
Parentheses around a binding name never change what it binds — on every store-target route:
plain and augmented assignment, annotated declarations, nested tuple elements, starred operands,
for and comprehension targets, and with … as targets. The parser canonicalizes the target once,
so (a) = 1 is exactly a = 1 (the canonical-form contract, #1170; Python accepts every spelling):
for (x) in [1, 2]:
print(x) # 1, 2
for ((a), b) in [(3, 4)]:
print(a, b) # 3 4
(c): int = 5
((d), e) = (6, 7)
(f), g = 8, 9
*(h), i = [10, 11, 12]
print(h, i) # [10, 11] 12
print([y for (y) in [13, 14]]) # [13, 14]
The shapes Python rejects stay rejected: (*a), b = xs ("cannot use starred expression here"),
except E as (e), and a parenthesized walrus target ((a) := 1).
Nested Tuple Unpacking¶
Targets can themselves be tuple patterns, enabling nested destructuring:
Nesting can be arbitrarily deep:
t: tuple[tuple[int, tuple[int, int]], int] = ((1, (2, 3)), 4)
(a, (b, c)), d = t
print(a) # 1
print(b) # 2
print(c) # 3
print(d) # 4
Each nested target must match the structure and element count of the corresponding tuple element.
Implementation
- 🔄 Lowered - Temporary variables with .Item1, .Item2, etc. access:
Rest Patterns (*rest)¶
A starred expression collects remaining elements into a list:
items: list[int] = [1, 2, 3, 4, 5]
# Collect tail
first, *rest = items
print(first) # 1
print(rest) # [2, 3, 4, 5]
# Collect head
*rest, last = items
print(rest) # [1, 2, 3, 4]
print(last) # 5
# Collect middle
first, *mid, last = items
print(first) # 1
print(mid) # [2, 3, 4]
print(last) # 5
Rules:
- Only one starred expression is allowed per unpacking
- The starred variable is always typed as list[T] where T is the element type of the source
- Works with both lists and tuples as the source
Implementation - 🔄 Lowered - Index access and slicing:
var __t0 = items;
var first = __t0[0];
var mid = __t0.GetSlice(new global::Sharpy.Slice((int?)1, (int?)-1));
var last = __t0[-1];
Tuple Unpacking in For Loops¶
Iterate over collections of tuples with destructuring:
pairs: list[tuple[str, int]] = [("alice", 1), ("bob", 2)]
for name, score in pairs:
print(f"{name}: {score}")
# alice: 1
# bob: 2
Nested unpacking is also supported in for loops:
items: list[tuple[tuple[int, int], str]] = [((1, 2), "a"), ((3, 4), "b")]
for (x, y), label in items:
print(f"{label}: {x + y}")
# a: 3
# b: 7
Implementation
- ✅ Native (simple case) - C# foreach (var (name, score) in pairs)
- 🔄 Lowered (nested case) - Temporary loop variable with .Item1, .Item2 access
Tuple Unpacking in Comprehensions¶
List, set, and dict comprehensions support tuple unpacking in their for clauses:
pairs: list[tuple[int, int]] = [(1, 2), (3, 4), (5, 6)]
sums = [a + b for a, b in pairs]
print(sums) # [3, 7, 11]
Nested unpacking works in comprehensions as well:
items: list[tuple[tuple[int, int], str]] = [((1, 2), "a"), ((3, 4), "b")]
result: list[str] = [name + ":" + str(x + y) for (x, y), name in items]
print(result) # ["a:3", "b:7"]
Implementation - 🔄 Lowered - Lambda with temporary variable destructuring:
Type Inference¶
Unpacking targets are automatically inferred from the source type:
| Source Type | Target Inference |
|---|---|
tuple[int, str] |
First target: int, second: str |
list[tuple[int, str]] (in for loop) |
Loop targets: int, str |
*rest from list[T] |
Starred target: list[T] |
*rest from tuple[T, ...] |
Starred target: list[T] (uses first element type) |
Nested tuple targets recurse into the corresponding element type and validate structure at each level.
Constructing Tuples: No tuple(iterable)¶
A tuple's arity is part of its type — tuple[int, str] lowers to a .NET ValueTuple with exactly
two fields. Python's tuple(iterable) produces a tuple as long as the iterable, which is a runtime
property, so there is no type to give the result. The form is rejected (SPY0338):
xs: list[int] = [1, 2, 3]
t = tuple(xs) # ❌ ERROR SPY0338: variable-length tuple(iterable) is not supported
Use list(...) when the length is a runtime value, or a tuple literal when the arity is known:
xs: list[int] = [1, 2, 3]
ys: list[int] = list(xs) # runtime length
t: tuple[int, int, int] = (xs[0], xs[1], xs[2]) # known arity
If the argument is already a tuple, the conversion is redundant — drop the call. Tuple literals, explicitly parameterized construction, and every unpacking form above are unaffected.
Deliberate CPython divergence (Axiom 1/3 over Axiom 2): explicitly parameterized construction converts elements to the written element types —
tuple[float, str]((1, "a"))is(1.0, 'a'). CPython'stuple[float, str]is a baretypes.GenericAliaswhose call ignores the parameters and would keep1anint. Sharpy treats the written annotation as the type authority, so the element widens exactly asx: float = 1does (#1200; pinned inParameterizedTupleConversionTests).
Element Stores Use the Target's Declared Type¶
When unpacking a tuple literal into targets that already have a declared type, each
element is a store into the target's declared slot. The element expression sees the
declared type as its expected type, so constructor shorthands like Some(v) and None()
can infer their type parameter from it:
class Box:
v: int? = None()
def main() -> None:
b: Box = Box()
n: int = 0
b.v, n = Some(5), 1 # Some(5) infers int? from b.v's declared type
print(b.v, n) # 5 1
Similarly, bare None into a nullable target adopts the target's type:
def main() -> None:
x: str | None = "hello"
x, n = None, 1 # None stores into str | None
print(x is None) # True
The R-T payload rule applies to identifier targets that are narrowed: inside a
narrowing block (if x is not None:), storing a payload value into a narrowed T?
variable wraps it — mirroring the behavior of plain assignments:
def main() -> None:
d: int? = Some(10)
if d is not None:
d, n = 5, 2 # 5 wraps to Some(5) — d stays int?
print(d, n) # 5 2
Refusals use the store seam's diagnostic codes: SPY0604 (strict Optional construction),
SPY0229 (None into non-nullable), not a generic type-mismatch.
Error Cases¶
| Scenario | Diagnostic |
|---|---|
| Element count mismatch | SPY0239: Cannot unpack N values into M variables |
| Unpacking a non-tuple type | SPY0239: Cannot unpack non-tuple type |
| Multiple starred expressions | SPY0356: Only one starred expression allowed |
tuple(iterable) construction |
SPY0338: variable-length tuple(iterable) is not supported |
See Also¶
- Spread Operator — Spreading collections with
*and** - Comprehensions — List, dict, and set comprehensions
- For Statement — For loop syntax