NonEmptyList¶
fptk.adt.nelist provides NonEmptyList, a list that is guaranteed to have at least one element by construction.
Concept: Non-Empty Collections¶
Many operations on lists fail or produce meaningless results when the list is empty:
max([]) # ValueError: max() arg is an empty sequence
min([]) # ValueError
head = xs[0] # IndexError if empty
sum(xs) / len(xs) # ZeroDivisionError if empty
A NonEmptyList makes non-emptiness a type-level guarantee. If you have a NonEmptyList, you know it has at least one element—no runtime checks needed.
The Problem: Empty List Checks¶
def average(xs: list[float]) -> float:
if not xs:
raise ValueError("Cannot compute average of empty list")
return sum(xs) / len(xs)
def first(xs: list[T]) -> T:
if not xs:
raise ValueError("List is empty")
return xs[0]
# Every function needs to validate, every caller needs to handle
The NonEmptyList Solution¶
from fptk.adt.nelist import NonEmptyList
def average(xs: NonEmptyList[float]) -> float:
# No check needed—xs is guaranteed non-empty
return sum(xs) / len(list(xs))
def first(xs: NonEmptyList[T]) -> T:
return xs.head # Always safe
# Construct safely
result = NonEmptyList.from_iter(data) # Option[NonEmptyList]
if result:
avg = average(result)
else:
# Handle empty case once, at the boundary
API¶
Types¶
| Type | Description |
|---|---|
NonEmptyList[E] |
List with at least one element |
Constructor¶
from fptk.adt.nelist import NonEmptyList
# Direct construction (always non-empty)
nel = NonEmptyList(1) # [1]
nel = NonEmptyList(1, (2, 3, 4)) # [1, 2, 3, 4]
# From iterable (might be empty)
result = NonEmptyList.from_iter([1, 2]) # NonEmptyList or None
result = NonEmptyList.from_iter([]) # None
Properties¶
| Property | Type | Description |
|---|---|---|
head |
E |
First element (guaranteed to exist) |
tail |
tuple[E, ...] |
Remaining elements (may be empty) |
Methods¶
| Method | Signature | Description |
|---|---|---|
append(e) |
(E) -> NonEmptyList[E] |
Add element at end |
to_list() |
() -> list[E] |
Convert to regular list |
from_iter(it) |
staticmethod (Iterable[E]) -> NonEmptyList[E] | None |
Create from iterable |
__iter__() |
() -> Iterator[E] |
Iterate all elements |
How It Works¶
Data Structure¶
NonEmptyList stores a required head and optional tail:
@dataclass(frozen=True, slots=True)
class NonEmptyList[E]:
head: E # First element (required)
tail: tuple[E, ...] = () # Remaining elements (tuple for immutability)
The head field is required, guaranteeing at least one element. The tail is a tuple (immutable) that may be empty.
Safe Construction¶
@staticmethod
def from_iter(it: Iterable[E]) -> NonEmptyList[E] | None:
iterator = iter(it)
try:
h = next(iterator)
except StopIteration:
return None # Empty iterable
return NonEmptyList(h, tuple(iterator))
from_iter returns None for empty iterables—the only way to get a NonEmptyList is with at least one element.
Iteration¶
Iterates in order: head first, then tail elements.
Append¶
Returns a new NonEmptyList with the element added at the end (immutable).
Examples¶
Safe Head Access¶
from fptk.adt.nelist import NonEmptyList
# Regular list: might fail
def unsafe_head(xs: list[int]) -> int:
return xs[0] # IndexError if empty!
# NonEmptyList: always safe
def safe_head(xs: NonEmptyList[int]) -> int:
return xs.head # Guaranteed to exist
# Construct at boundaries
data = get_data() # list[int]
nel = NonEmptyList.from_iter(data)
if nel:
print(safe_head(nel))
else:
print("No data available")
Computing Statistics¶
from fptk.adt.nelist import NonEmptyList
def stats(xs: NonEmptyList[float]) -> dict:
"""Compute statistics. No empty-list checks needed."""
values = list(xs)
return {
"count": len(values),
"sum": sum(values),
"mean": sum(values) / len(values),
"min": min(values), # Safe
"max": max(values), # Safe
"first": xs.head, # Safe
}
# Safe construction
data = NonEmptyList.from_iter(measurements)
if data:
result = stats(data)
Building Results¶
from fptk.adt.nelist import NonEmptyList
def collect_errors(validations: list[Result]) -> NonEmptyList[str] | None:
"""Collect error messages, if any."""
errors = [r.error for r in validations if r.is_err()]
return NonEmptyList.from_iter(errors)
# Later
errors = collect_errors(results)
if errors:
# We know there's at least one error
print(f"First error: {errors.head}")
print(f"Total errors: {len(list(errors))}")
With Validation¶
from fptk.validate import validate_all
from fptk.adt.nelist import NonEmptyList
# validate_all returns Result[T, NonEmptyList[E]]
# If validation fails, you're guaranteed at least one error
result = validate_all([check1, check2, check3], data)
result.match(
ok=lambda d: process(d),
err=lambda errors: print(f"Validation failed: {errors.head}")
# errors is NonEmptyList[str], so .head is safe
)
Chaining Operations¶
from fptk.adt.nelist import NonEmptyList
# Build up a list
nel = NonEmptyList(1)
nel = nel.append(2).append(3).append(4)
print(nel.head) # 1
print(nel.tail) # (2, 3, 4)
print(list(nel)) # [1, 2, 3, 4]
Converting Collections¶
from fptk.adt.nelist import NonEmptyList
# From various iterables
from_list = NonEmptyList.from_iter([1, 2, 3])
from_set = NonEmptyList.from_iter({1, 2, 3})
from_gen = NonEmptyList.from_iter(x for x in range(5))
# To list
nel = NonEmptyList(1, (2, 3))
regular_list = nel.to_list() # [1, 2, 3]
When to Use NonEmptyList¶
Use NonEmptyList when:
- Your domain requires at least one element
- You want to eliminate empty-list checks in downstream code
- You're accumulating errors (validation)
- Computing aggregates that require non-empty input (mean, max, etc.)
Don't use NonEmptyList when:
- Empty collections are valid in your domain
- You need frequent random access (use list)
- You need efficient append (tuple concatenation is O(n))
NonEmptyList vs Option[list]¶
| Type | Meaning |
|---|---|
list[T] |
Zero or more elements |
Option[list[T]] |
Maybe a list (but list could still be empty!) |
NonEmptyList[T] |
One or more elements (guaranteed) |
Option[NonEmptyList[T]] |
Maybe a non-empty list |
NonEmptyList is the right choice when you need to guarantee non-emptiness at the type level.
See Also¶
validate_all— Uses NonEmptyList for error accumulationOption— For values that might be absentResult— For computations that might fail