# How to Convert OpenEnv Async EnvClient to Synchronous with the `.sync()` Method

> Easily convert your OpenEnv async EnvClient to synchronous using the .sync() method. Get a blocking synchronous API for your WebSocket operations.

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

---

**Call `.sync()` on any OpenEnv `EnvClient` instance to receive a `SyncEnvClient` wrapper that runs async WebSocket operations in a background thread, exposing a blocking synchronous API.**

OpenEnv's `EnvClient` is deliberately asynchronous to maintain persistent WebSocket connections without blocking the caller. For developers working in synchronous codebases, standard Python scripts, or REPL environments, the huggingface/OpenEnv repository provides a built-in conversion mechanism that wraps the async client while preserving full functionality.

## Why OpenEnv Uses Async by Default

The `EnvClient` class in [`src/openenv/core/env_client.py`](https://github.com/huggingface/OpenEnv/blob/main/src/openenv/core/env_client.py) employs async/await patterns to handle long-lived WebSocket connections and Docker provisioning efficiently. This architecture prevents I/O blocking during network operations but requires `async` and `await` keywords throughout consuming code. For libraries and scripts that cannot accommodate an async event loop, the `.sync()` factory method offers a seamless bridge.

## The `.sync()` Method Architecture

### How `EnvClient.sync()` Creates the Wrapper

Defined at line 85 in [`src/openenv/core/env_client.py`](https://github.com/huggingface/OpenEnv/blob/main/src/openenv/core/env_client.py), the `.sync()` method imports and returns a `SyncEnvClient` instance. This method passes the original async client (`self`) to the wrapper, which manages thread-safe communication between your synchronous code and the underlying async implementation.

### The `SyncEnvClient` Implementation

The `SyncEnvClient` class in [`src/openenv/core/sync_client.py`](https://github.com/huggingface/OpenEnv/blob/main/src/openenv/core/sync_client.py) creates a dedicated `asyncio` event loop running in a separate daemon thread. Key implementation details include:

- **Coroutine Scheduling**: Uses `asyncio.run_coroutine_threadsafe` to dispatch async method calls from the main thread to the background loop.
- **Dynamic Method Wrapping**: Implements `__getattr__` to automatically wrap any coroutine-based method on the underlying async client, caching the synchronous wrapper for subsequent invocations.
- **Blocking Execution**: Every method call (`reset`, `step`, `state`, etc.) blocks until the coroutine completes, returning the result directly to the caller.

### Resource Management and Cleanup

`SyncEnvClient` implements the context manager protocol with `__enter__` and `__exit__` methods. The `close()` method ensures proper teardown by:

1. Calling the async client's `close()` coroutine.
2. Stopping the background event loop cleanly.
3. Preventing zombie daemon threads from persisting after client usage.

## Practical Code Examples

### Basic Synchronous Usage with Context Managers

```python
from echo_env import CallToolAction, EchoEnv

# Convert async client to synchronous

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

with sync_client:
    # Blocking reset call

    result = sync_client.reset()
    print(result.observation.echoed_message)
    
    # Blocking step call

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

```

### Manual Connection Lifecycle

```python
from openenv.core import GenericEnvClient

# Create sync wrapper from GenericEnvClient

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

# Explicitly manage WebSocket lifecycle

client.connect()          # Establishes connection in background loop

obs = client.reset()      # Blocking execution

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

client.close()            # Terminates background event loop

```

### Hybrid Async and Sync Access

```python
async_client = EchoEnv(base_url="...")
sync_client = async_client.sync()

# Use synchronous wrapper for main workflow

result = sync_client.reset()

# Access underlying async methods when needed

await async_client.some_async_helper()

```

## Supported Client Types

The `.sync()` method is available across the OpenEnv client hierarchy:

- **GenericEnvClient** ([`src/openenv/core/generic_client.py`](https://github.com/huggingface/OpenEnv/blob/main/src/openenv/core/generic_client.py)): The base implementation for generic environment connections.
- **Environment-specific clients**: Concrete implementations like `EchoEnv` that inherit from `EnvClient` and expose the same synchronous conversion capability via inheritance.

## Summary

- OpenEnv's `EnvClient` uses async architecture for non-blocking WebSocket and Docker operations.
- The `.sync()` method in [`src/openenv/core/env_client.py`](https://github.com/huggingface/OpenEnv/blob/main/src/openenv/core/env_client.py) returns a `SyncEnvClient` wrapper that runs a dedicated event loop in a background daemon thread.
- Method calls use `asyncio.run_coroutine_threadsafe` to bridge sync and async execution transparently.
- Context manager support via `__enter__` and `__exit__` ensures proper resource cleanup through the `close()` method.
- All async clients—including `GenericEnvClient` and environment-specific implementations—support the synchronous conversion pattern.

## Frequently Asked Questions

### Does using `.sync()` impact performance compared to native async?

The synchronous wrapper introduces minimal overhead through thread scheduling and blocking synchronization primitives. While native async remains optimal for high-concurrency scenarios, the `.sync()` wrapper provides comparable performance for standard sequential workflows and blocking operations.

### Can I use the async client and sync wrapper simultaneously?

Yes. Calling `.sync()` creates a wrapper that references the original async client. You can interleave calls between the `async_client` (using `await`) and `sync_client` (blocking), though you should consider thread-safety when accessing shared state between the two interfaces.

### What happens if I don't call `close()` on the sync client?

The `SyncEnvClient` daemon thread will continue running until the Python process terminates. Using the context manager (`with` statement) or explicitly calling `close()` ensures the background event loop stops cleanly and releases the WebSocket connection immediately.

### Is `.sync()` available on all OpenEnv clients?

Any class inheriting from `EnvClient`—including `GenericEnvClient` and environment-specific implementations like `EchoEnv`—inherits the `.sync()` method defined in [`src/openenv/core/env_client.py`](https://github.com/huggingface/OpenEnv/blob/main/src/openenv/core/env_client.py) and can generate a synchronous wrapper.