Skip to content

Migrating from 2.* to 3.*

How to Read This Guide

This guide is intended to help you migrate existing functionality from that-depends version 2.* to 3.*.
The goal is to enable you to migrate as quickly as possible while making only the minimal necessary changes to your codebase.

If you want to learn more about the new features introduced in 3.*, please refer to the documentation and the release notes.


Deprecated or Removed Features

container_context() can no longer be initialized without arguments.

Previously the following code would reset the context for all all providers in all containers:

async with container_context():
    # all ContextResources have been reset
    ...
This was done to enable easy migration and compatibility with 1.*.

Now container_context must be called with at least 1 argument or keyword-argument. Thus if you want to reset the context for all containers you need to provide them explicity:

async with container_context(MyContainer_1, MyContainer_2, ...):
    ...


container_context() no longer accepts reset_all_containers keyword argument.

You can no longer reset the context for all containers by using the container_context context manager. Previously you could have done something like this:

async with container_context(reset_all_containers=True):
    # all ContextResources have been reset
    ...
Now you will need to explicitly pass the containers to the container context:
async with container_context(MyContainer_1, MyContainer_2, ...):
    ...


@inject(scope=...) no longer enters the scope.

The @inject decorator no longer enters the scope specified in the scope argument.

In 2.* the provided scope would be entered before the function was called:

@inject(scope=ContextScopes.REQUEST)
def injected(...): ...
    assert get_current_scope() == ContextScopes.REQUEST

In 3.* the scope is only used to resolve relevant dependencies that match the provided scope. Thus, to achieve the same behaviour as in 2.* you need to set enter_scope=True:

@inject(scope=ContextScopes.REQUEST, enter_scope=True)
def injected(...): ...
    assert get_current_scope() == ContextScopes.REQUEST
For further details, please refer to the scopes documentation


Changes in the API

Changes to naming of methods.

You can expect the default implementation of provider and container methods to be async. This means that methods not explicitly ending with _sync are normally async.

This has also introduced new interfaces for operations where part of the implementation was missing (only async or sync implementation was provided in 2.*).

For example tear_down() is now async per default and tear_down_sync() has been introduced.

Other examples of similar changes include:

  • .async_resolve() -> .resolve()
  • .sync_resolve() -> .resolve_sync()

Tear down propagation enabled per default.

Tear down is now propagated to all dependencies by default.

Previously if you called tear down for a provider this only reset the given provider.

await provider.tear_down()
In order to maintain this behaviour in 3.*:
await provider.tear_down(propagate=False)
For more details regarding tear-down propagation see the documentation.


Overriding is now async per default.

As mentioned above, .override() methods are now async per default.

Thus any instances of the following in 2.*:

provider.override(value)
Should be changed to the following in 3.*:
provider.override_sync(value)

This is also the case for the following methods:

  • .override_context() -> .override_context_sync()
  • .reset_override() -> .reset_override_sync()

Note: Overrides now support tear-down, read more in the documentation


Further Help

If you continue to experience issues during migration, consider creating a discussion or opening an issue.