Why Not sync_to_async?

If you are asked to have async and synchronous variants for your API, it might be tempting to reach for asgiref’s sync_to_async wrapper.

While sync_to_async provides a helpful last-resort tool to offer an async API, it has a couple issues.

sync_to_async prevents

Backwards Compatibility Issues In Subclasses

Imagine you have the following class in a Django library:

class ThingDoer:
  def do_thing(self):
    self.step1()
    self.step2()
    self.step3()

  async def ado_thing(self):
    await sync_to_async(self.do_thing)()

A user of ThingDoer subclasses it to add their own custom step:

class SpecialThingDoer(ThingDoer):
  def do_thing(self):
    super().do_thing()
    self.step4()

At this point something odd has happened. SpecialThingDoer.ado_thing will _also_ use step4!

At first this seems great. After all, the user wants to have step4 happen and since it was specified in do_thing, you “probably” want it in step4

At one point you realise that step2 can be made async! Of course in order to do so we can no longer blindly call sync_to_async:

class ThingDoer:
  def do_thing(self):
    self.step1()
    self.step2()
    self.step3()

  async def ado_thing(self):
    self.step1()
    await self.astep2()
    self.step3()

Here we broke out separate implementations, in order to actualy await on the second step.

This change just broke SpecialThingDoer! While before it was able to rely on ado_thing calling into do_thing, this is no longer the case.

Of course, in this new model, ThingDoer subclasses now have to maintain two method overrides to handle their extra step. In practice this can be extremely tedious (hence django-unasyncify’s existence in the first place). So one might still opt for sync_to_async in circumstances where one really doesn’t expect (or want) async operations to occur.

But if you want to hold out for a future where async makes sense, you probably want to avoid sync_to_async.