# How to Convert an Async OpenEnv EnvClient to Synchronous Usage with the `.sync()` Wrapper

> Easily use OpenEnv's async EnvClient synchronously. Learn how the .sync() wrapper safely converts async methods like reset() and step() for seamless integration without blocking your main thread.

- Repository: [Hugging Face/OpenEnv](https://github.com/huggingface/OpenEnv)
- Tags: how-to-guide
- Published: 2026-06-14

---

**OpenEnv provides a thin synchronous façade via the `.sync()` method that wraps the async `EnvClient` in a dedicated background event loop, allowing you to use `reset()`, `step()`, and other methods synchronously without blocking your main thread.**

The `huggingface/OpenEnv` library defines its core environment client (`EnvClient`) as an asynchronous abstraction to maintain persistent WebSocket connections without blocking the caller. For scripts, REPLs, or libraries that require classic synchronous flow, OpenEnv offers a seamless conversion pathway through the `.sync()` wrapper method.

## Understanding the Async-to-Sync Architecture

OpenEnv’s synchronous support relies on two primary components working together to bridge the async/sync divide while keeping the underlying WebSocket and Docker provisioning logic unchanged.

### The `sync()` Factory Method

In [`src/openenv/core/env_client.py`](https://github.com/huggingface/OpenEnv/blob/main/src/openenv/core/env_client.py) at line 85, the abstract `EnvClient` class defines the **`sync()`** method. This factory imports and returns an instance of `SyncEnvClient`, passing the original async client (`self`) to the wrapper. This design allows any concrete client inheriting from `EnvClient` to expose synchronous capabilities instantly.

### The `SyncEnvClient` Wrapper Class

Implemented in **[`src/openenv/core/sync_client.py`](https://github.com/huggingface/OpenEnv/blob/main/src/openenv/core/sync_client.py)**, the `SyncEnvClient` class creates a dedicated background `asyncio` event loop running in its own daemon thread. When you call synchronous methods like `reset()` or `step()`, the wrapper schedules the underlying coroutine via `asyncio.run_coroutine_threadsafe` and blocks until the result is available.

The class implements `__getattr__` to automatically wrap any coroutine-based method on the async client, caching the sync wrapper for subsequent calls. This ensures that new methods added to the async client become immediately available in the synchronous interface without additional boilerplate.

## Converting Your Async Client to Synchronous

To convert async OpenEnv EnvClient to synchronous usage, instantiate your client as usual and chain the `.sync()` method. The resulting object behaves like a standard blocking Python API.

### Basic Synchronous Usage

The most common pattern uses the context manager protocol to ensure proper cleanup:

```python
from echo_env import CallToolAction, EchoEnv

# Obtain a synchronous wrapper from an async client

sync_client = EchoEnv(base_url="https://openenv-echo-env.hf.space").sync()

# Use it as a normal context manager

with sync_client:
    # Reset the environment (synchronous)

    result = sync_client.reset()
    print(result.observation.echoed_message)  # → "Echo environment ready!"

    # Step the environment

    result = sync_client.step(
        CallToolAction(
            tool_name="echo_message",
            arguments={"message": "Hello, World!"},
        )
    )
    print(result.observation.result)  # → "Hello, World!"

```

### Explicit Connection Management

For scenarios requiring manual control over the WebSocket lifecycle, call `connect()` and `disconnect()` explicitly before closing the wrapper:

```python
from openenv.core import GenericEnvClient

# Create an async client and wrap it

client = GenericEnvClient(base_url="ws://localhost:8000").sync()

# Manually manage the connection

client.connect()          # Establishes the WebSocket in the background loop

obs = client.reset()
obs = client.step({"code": "print('hello')"})
client.disconnect()       # Closes the WebSocket

client.close()            # Stops the background event loop

```

### Accessing Async-Only Methods

Even after wrapping, you retain access to the underlying async client for mixed-mode workflows:

```python
async_client = EchoEnv(base_url="...")          # async client

sync_client = async_client.sync()               # sync wrapper

# Directly call an async method (still async)

await async_client.some_async_helper()

```

## Resource Management and Cleanup

The `SyncEnvClient` implements the context-manager protocol (`__enter__`/`__exit__`) and exposes a `close()` method that properly terminates resources. When invoked, these methods call the async client’s `close()` coroutine and stop the background event loop cleanly, ensuring no stray daemon threads remain. Always use the context manager pattern or explicitly call `close()` when finished to prevent thread leaks.

## Summary

- **File location**: The `.sync()` method is defined in [`src/openenv/core/env_client.py`](https://github.com/huggingface/OpenEnv/blob/main/src/openenv/core/env_client.py) (line 85), returning a `SyncEnvClient` from [`src/openenv/core/sync_client.py`](https://github.com/huggingface/OpenEnv/blob/main/src/openenv/core/sync_client.py).
- **Mechanism**: The wrapper runs a dedicated `asyncio` event loop in a background daemon thread, using `asyncio.run_coroutine_threadsafe` to bridge sync calls to async methods.
- **Automatic wrapping**: `__getattr__` automatically makes any async method available as a synchronous equivalent without manual registration.
- **Cleanup**: Use context managers (`with` statement) or call `close()` to stop the background loop and prevent resource leaks.

## Frequently Asked Questions

### How do I convert an async OpenEnv EnvClient to synchronous usage without rewriting my codebase?

Call the `.sync()` method on any `EnvClient` instance to receive a `SyncEnvClient` wrapper that exposes the same API as synchronous methods. This wrapper handles all `async/await` complexity internally by running a background event loop in a separate thread, allowing you to use standard blocking calls like `reset()` and `step()` immediately.

### Does the synchronous wrapper support all methods available on the async client?

Yes. The `SyncEnvClient` class in [`src/openenv/core/sync_client.py`](https://github.com/huggingface/OpenEnv/blob/main/src/openenv/core/sync_client.py) implements `__getattr__` to dynamically wrap any coroutine-based method it encounters. This means new methods added to `EnvClient` subclasses automatically become available in the synchronous interface without requiring updates to the wrapper code.

### What happens if I don't close the synchronous client properly?

Failing to close the `SyncEnvClient` leaves a daemon thread running with an active `asyncio` event loop, which can prevent your program from exiting cleanly or cause resource leaks. Always use the context manager protocol (`with sync_client:`) or explicitly call `sync_client.close()` to shut down the background loop and release the WebSocket connection.

### Can I mix synchronous and asynchronous calls in the same workflow?

Yes. You can maintain references to both the original async client and the wrapped synchronous client simultaneously. After calling `sync_client = async_client.sync()`, you can still await `async_client` methods directly while using `sync_client` for blocking operations, enabling hybrid async/sync architectures within the same process.