Inject by type hint
Mark a class with @Injectable(), register it in a module's providers, and ask for it in a constructor. When the controller is created, the container inspects the service's own constructor, resolves its dependencies first, and so on down the graph.
from austial import Controller, Injectable
@Injectable()
class CatsService:
def __init__(self) -> None:
self._cats: list[Cat] = []
@Controller("cats")
class CatsController:
def __init__(self, cats_service: CatsService):
self.cats_service = cats_serviceScopes
Providers are singletons by default: the first resolution builds and caches the instance, and every later resolution returns the same object. Use Scope.TRANSIENT when you want a fresh instance each time it is resolved.
Austial does not implement a request scope yet. The docs say so plainly, so you do not go looking for it.
| Scope | Behaviour |
|---|---|
| Scope.DEFAULT | Singleton, one instance for the lifetime of the application |
| Scope.TRANSIENT | A new instance is built every time it is resolved |
Custom providers
When a plain class is not enough, a provider can be a dict with a provide key. Bind a token to a fixed value, to another class for swapping implementations per environment, or to the result of a factory function with its own injected dependencies.
providers=[
{"provide": "API_KEY", "useValue": "super-secret"},
{"provide": CacheService, "useClass": RedisCacheService},
{
"provide": "DATABASE_URL",
"useFactory": lambda config: config.get("DATABASE_URL", "sqlite+aiosqlite:///./app.db"),
"inject": [ConfigService],
},
]How it differs from FastAPI's Depends()
Depends() resolves functions per request, wired at each route. Austial's container resolves classes through their constructors once, at the application level, so a service is declared in one place and injected wherever its type is requested. The testing module can then build a real graph from any subset of providers.
Global modules and module visibility
A module marked @Global() makes its exports available everywhere without every consumer listing it in imports. austial.config uses this for ConfigModule, and the security, tenancy and jobs modules are global too.
One honest detail: today the container is application-wide and flat. Every module's providers register into one container regardless of imports and exports, so those lists are used for scanning but are not yet enforced as a visibility boundary. Write them correctly anyway, so nothing changes in your code if strict visibility lands.
from austial import Global, Module
@Global()
@Module(providers=[ConfigService], exports=[ConfigService])
class ConfigModule:
passDynamic modules for configurable providers
A dynamic module computes its imports, controllers and providers at runtime from a factory method. This is how ConfigModule.for_root(), OrmModule.for_root() and the other framework modules take configuration, and it is the pattern to follow for your own reusable modules. The async variants accept a factory with injected dependencies, so a database URL can be read from ConfigService.
OrmModule.for_root_async(
use_factory=lambda config: {
"type_": "postgres",
"url": config.get("DATABASE_URL"),
},
inject=[ConfigService],
entities=[Cat],
)Frequently asked questions
Does Austial need decorators on every dependency?
No. For the common case, an @Injectable() class registered as a provider and a type-hinted constructor parameter are enough.
Can I swap an implementation in tests?
Yes. The testing module builds a real, fully wired application from a set of controllers and providers, so you choose which implementations are in the graph.
What happens with circular dependencies?
The container detects the cycle during resolution and raises a CircularDependencyError that describes it, instead of recursing forever. The preferred fix is to redesign, for example by moving shared logic into a third provider.