Here’s a succinct definition of a first() function for Python:

# © 2023 Alex DeLorenzo. Licensed under the LGPLv3.
from typing import Iterable, TypeVar


T = TypeVar('T')
U = TypeVar('U')

Item = T | U | None


def first(iterable: Iterable[T], default: Item = None) -> Item:
  iterator = iter(iterable)
  return next(iterator, default)

Scroll to the bottom or click here to see an async implementation of this first() function.

Using the function

Here’s a look at how to use first() with various data structures that are common in Python.

Iterables

gen_nums: Iterable[int] = range(10)
first_num: int = next(gen_nums)  # 0

assert first(gen_nums) == first_num

Ordered sequences

nums: list[int] = list(gen_nums)

assert first(nums) == first_num

Empty sequences

empty: tuple = ()

assert first(empty) is None
assert first(range(0)) is None

Default value

The default value can be any object, not just None.

class Missing: pass

assert first(empty, Missing) is Missing

Generator expressions

assert first(num for num in gen_nums) == first_num

Strings and bytes

assert first('example') == 'e'
assert first(b'example') == b'e'

Arrays

from array import array

nums: Iterable[int] = array('i', gen_nums)

assert first(nums) == first_num

Files

from pathlib import Path

nums = map(str, gen_nums)
text = '\n'.join(nums)

path = Path('example.txt')
path.write_text(text)

with path.open() as file:
  assert first(file) == f'{first_num}\n'

Custom iterators

from typing import Iterator
from dataclasses import dataclass

@dataclass
class Increment(Iterator[int]):
  val: int = gen_nums.start

  def __next__(self) -> int:
    self.val += gen_nums.step
    return self.val

nums: Iterable[int] = Increment()

assert first(nums) == first_num

Unordered collections

Doesn’t make much sense with unordered collections, but it works. It could be used as a way to get a member of a set without raising a KeyError if it’s empty, or to supply a default value.

nums: set[int] = set(gen_nums)

assert first(nums) is not None

An async first function

from typing import AsyncIterable


async def first(iterable: AsyncIterable[T], default: Item = None) -> Item:
  aiterator = aiter(iterable)
  return await anext(aiterator, default)

Usage

To run these examples with top-level await expressions, you can use an async REPL by running python -m asyncio, or by using IPython.

Files

Here’s an example using 📁 aiopath, an async pathlib replacement for Python.

from aiopath import AsyncPath

nums = map(str, gen_nums)
text = '\n'.join(nums)

path = AsyncPath('example.txt')
await path.write_text(text)

async with path.open() as file:
  assert await first(file) == f'{first_num}\n'

Async iterables

async def agen_nums() -> AsyncIterable[int]:
  for num in gen_nums:
    yield num

nums = agen_nums()

assert await first(nums) == first_num

Async generator expressions

nums: AsyncIterable[int] = agen_nums()

assert await first(num async for num in nums) == first_num

Custom async iterators

from typing import AsyncIterator
from dataclasses import dataclass

@dataclass
class Increment(AsyncIterator[int]):
  val: int = gen_nums.start

  async def __anext__(self) -> int:
    self.val += gen_nums.step
    return self.val

nums: AsyncIterable[int] = Increment()

assert await first(nums) == first_num