better-py

AsyncMaybe Monad

Async optional values with awaitable operations

The AsyncMaybe monad extends Maybe with async operations, supporting awaitable computations for optional values. It combines the benefits of async/await with the safety of optional value handling.

AsyncMaybe[T] wraps a Maybe[T] and provides async operations. All operations that might be async are awaitable, and it converts to/from regular Maybe.

Creating AsyncMaybes

Creating with Values

The AsyncMaybe.some method creates an AsyncMaybe containing a value.

from better_py import AsyncMaybe

async_maybe = AsyncMaybe.some(42)

Use this when you have a value to wrap in an AsyncMaybe context.

Creating Empty Values

The AsyncMaybe.nothing method creates an empty AsyncMaybe.

from better_py import AsyncMaybe

empty = AsyncMaybe.nothing()

Use this when you need to represent the absence of a value.

Creating from Optional Values

The AsyncMaybe.from_value method creates an AsyncMaybe from an optional value.

from better_py import AsyncMaybe

AsyncMaybe.from_value(42)      # Some(42)
AsyncMaybe.from_value(None)    # Nothing

Use this to convert existing optional values into AsyncMaybe.

Checking Variants

Checking for Values

The is_some_async method checks if the AsyncMaybe contains a value.

from better_py import AsyncMaybe

await AsyncMaybe.some(42).is_some_async()     # True
await AsyncMaybe.nothing().is_some_async()    # False

Use this to check for the presence of a value.

Checking for Empty

The is_nothing_async method checks if the AsyncMaybe is empty.

from better_py import AsyncMaybe

await AsyncMaybe.some(42).is_nothing_async()    # False
await AsyncMaybe.nothing().is_nothing_async()  # True

Use this to check for the absence of a value.

Extracting Values

Getting Values or Errors

The unwrap method returns the value or raises an error if empty.

from better_py import AsyncMaybe

await AsyncMaybe.some(42).unwrap()         # 42
await AsyncMaybe.nothing().unwrap()       # Raises ValueError

Use this only when you're certain the AsyncMaybe contains a value.

Getting Values or Defaults

The unwrap_or_else method returns the value or computes a default.

from better_py import AsyncMaybe

# With value default
await AsyncMaybe.some(42).unwrap_or_else(0)        # 42
await AsyncMaybe.nothing().unwrap_or_else(0)       # 0

# With function default
await AsyncMaybe.nothing().unwrap_or_else(lambda: expensive())  # expensive()

Use this when you need safe access to the value with a fallback.

Transforming Values

Mapping Values

The map method applies a function to the contained value.

from better_py import AsyncMaybe

AsyncMaybe.some(5).map(lambda x: x * 2)         # Some(10)
AsyncMaybe.nothing().map(lambda x: x * 2)       # Nothing

When map is called on Nothing, it returns Nothing unchanged.

Mapping Async Functions

The map_async method applies an async function to the contained value.

from better_py import AsyncMaybe

async def fetch(x: int) -> str:
    return await api.get(f"/items/{x}")

await AsyncMaybe.some(5).map_async(fetch)       # Some(result)
await AsyncMaybe.nothing().map_async(fetch)     # Nothing

Use this when the transformation function is async.

Chaining Operations

Binding Async Operations

The bind method chains AsyncMaybe-returning operations.

from better_py import AsyncMaybe

async def get_user(user_id: int) -> AsyncMaybe[dict]:
    user = await db.fetch(user_id)
    return AsyncMaybe.from_value(user)

async def get_orders(user: dict) -> AsyncMaybe[list]:
    orders = await db.fetch_orders(user["id"])
    return AsyncMaybe.from_value(orders)

# Chain operations
orders = await get_user(1).bind(get_orders)  # Some([...]) or Nothing

Use bind for sequential operations where each step depends on the previous one.

Converting to Maybe

Sync Conversion

The to_maybe method converts AsyncMaybe to a regular Maybe.

from better_py import AsyncMaybe, Some, Nothing

AsyncMaybe.some(42).to_maybe()          # Some(42)
AsyncMaybe.nothing().to_maybe()         # Nothing

Use this when you need to integrate with non-async code that uses Maybe.

Real-World Pattern: Async Database Queries

from better_py import AsyncMaybe

async def find_user(user_id: int) -> AsyncMaybe[dict]:
    user = await database.query(f"SELECT * FROM users WHERE id = {user_id}")
    return AsyncMaybe.from_value(user)

async def get_user_email(user_id: int) -> AsyncMaybe[str]:
    return (await find_user(user_id)
        .bind(async lambda user: AsyncMaybe.from_value(user.get("email"))))

async def get_user_orders(user_id: int) -> AsyncMaybe[list]:
    return (await find_user(user_id)
        .bind(async lambda user: fetch_orders(user["id"])))

# Use the chained operations
email = await get_user_email(1)  # Some("alice@example.com") or Nothing
orders = await get_user_orders(1)  # Some([...]) or Nothing

This pattern shows AsyncMaybe's power: async database operations that might not find data are handled gracefully, and operations can be chained without nested if statements.

When to Use

Use AsyncMaybe when:

  • Working with async code that returns optional values
  • You need awaitable operations
  • You're using asyncio
  • You want to chain async operations that might not return values
  • You need safe handling of missing async values

Don't use AsyncMaybe when:

  • You're not using async (use Maybe instead)
  • You need error messages (use AsyncResult instead)
  • Values should always be present (use plain types)
  • You're doing simple async operations (plain async/await is fine)

See Also

  • Maybe - Non-async optional values
  • AsyncResult - Async error handling
  • Try - Exception handling

On this page