What Wireup Validates
This page explains what Wireup validates and when those checks happen.
The summary is:
- Wireup validates the registration graph when the container is created.
- Wireup also validates injection targets it can inspect up front, such as
@inject_from_container(...)functions. - Wireup offers the same validation for integrations that it can inspect up front. Look on the integration's documentation for details on whether it does upfront validation.
- Dependency requests that are only decided later by your code can still fail when they happen.
- Wireup does not validate the runtime behavior inside your constructors and factories.
What "If The Container Starts, It Works" Means¶
When container creation succeeds, Wireup has already verified the following:
- Every required dependency is resolvable
- Config keys requested through
Inject(config=...)exist - There are no circular dependencies
- There are no duplicate registrations for the same type and qualifier
- Singleton services do not depend on scoped or transient services
- Factories are properly annotated with return types
@inject_from_container(...)functions are checked at module import time.
Runtime @inject usage in some integrations, or arbitrary
container.get(...) calls made by your own code cannot be inspected ahead of time.
Checked When The Container Is Created¶
These errors fail fast during container creation.
Duplicate Registration¶
Wireup rejects two registrations for the same type and qualifier if they come from different sources.
import wireup
from wireup import injectable
@injectable
class Foo: ...
@injectable
def make_foo() -> Foo:
return Foo()
wireup.create_sync_container(injectables=[Foo, make_foo])
Why it fails:
- Wireup found two different ways to build
Foo
Why this is bad:
Foois provided by multiple registrations and wireup would have to arbitrarily pick one.- Small registration or import changes could make your app start using a different implementation without you noticing
Missing Dependency¶
If a required dependency is not registered, Wireup fails as soon as it can inspect that injection path.
import wireup
from wireup import injectable
class Database: ...
@injectable
class UserRepository:
def __init__(self, db: Database) -> None:
self.db = db
wireup.create_sync_container(injectables=[UserRepository])
import wireup
from wireup import Injected, inject_from_container
class UserRepository: ...
container = wireup.create_sync_container(injectables=[])
@inject_from_container(container)
def run_job(repo: Injected[UserRepository]) -> None:
pass
import fastapi
import wireup
import wireup.integration.fastapi
from wireup import Injected
class UserRepository: ...
app = fastapi.FastAPI()
@app.get("/users")
def get_users(repo: Injected[UserRepository]) -> list[str]:
return []
container = wireup.create_sync_container(injectables=[])
wireup.integration.fastapi.setup(container, app)
Why it fails:
- Each example asks Wireup for
UserRepository, but nothing in the container knows how to create it
Why this is bad:
- The function or service cannot run because one of its required inputs is missing
Missing Config Key¶
Wireup validates config keys requested through Inject(config=...). Only the presence of the key is checked, not the
value itself.
from typing import Annotated
import wireup
from wireup import Inject, injectable
@injectable
class Database:
def __init__(self, dsn: Annotated[str, Inject(config="db_url")]) -> None:
self.dsn = dsn
wireup.create_sync_container(
injectables=[Database],
config={},
)
Why it fails:
Databaseasks fordb_url, but that key is missing from the config you passed to the container
Why this is bad:
- The dependency is missing, and
Databasecannot be created without that setting
Circular Dependency¶
Wireup detects dependency cycles in the registration graph.
import wireup
from wireup import injectable
@injectable
class Foo:
def __init__(self, bar: "Bar") -> None:
self.bar = bar
@injectable
class Bar:
def __init__(self, foo: Foo) -> None:
self.foo = foo
wireup.create_sync_container(injectables=[Foo, Bar])
Why it fails:
- To build
Foo, Wireup needsBar, and to buildBar, it needsFoo
Why this is bad:
- The dependency graph has no valid starting point, so Wireup cannot build neither
FoonorBar - Your app would get stuck trying to build services that depend on each other in a loop
Invalid Lifetime Dependency¶
A singleton cannot depend on a scoped or transient dependency.
import wireup
from wireup import injectable
@injectable(lifetime="scoped")
class RequestContext: ...
@injectable(lifetime="singleton")
class AuditService:
def __init__(self, ctx: RequestContext) -> None:
self.ctx = ctx
wireup.create_sync_container(injectables=[RequestContext, AuditService])
Why it fails:
AuditServicewould keep aRequestContextthat is only meant to live for one request
Why this is bad:
- Later requests could end up using old request data by mistake
- The object would outlive the thing it depends on, which usually leads to stale state and hard-to-track bugs
Factory Without A Return Type¶
Wireup uses the return type to determine which service the factory provides.
import wireup
from wireup import injectable
@injectable
def make_foo():
return Foo()
wireup.create_sync_container(injectables=[make_foo])
Why it fails:
- Wireup can see the factory, but it cannot tell what service that function is supposed to provide
Why this is bad:
- There is no clear type to register this factory under
- Different readers and tools would have to guess what
make_foois meant to produce
Missing Type Annotations¶
Wireup requires type annotations to determine what to inject.
import wireup
from wireup import injectable
@injectable
class UserRepository:
def __init__(self, db) -> None:
self.db = db
wireup.create_sync_container(injectables=[UserRepository])
Why it fails:
- The
dbparameter has no type annotation, so Wireup does not know what to look up and inject
Why this is bad:
- Wireup would have to guess what
dbmeans
Positional-Only Parameters¶
Wireup injects by keyword argument, so positional-only parameters are rejected.
import wireup
from wireup import injectable
class Database: ...
@injectable
def make_repo(db: Database, /) -> UserRepository:
return UserRepository(db=db)
wireup.create_sync_container(injectables=[make_repo])
Why it fails:
dbcan only be passed by position, but Wireup injects dependencies by parameter name
Why this is bad:
- Wireup cannot provide dependencies to that function
Invalid as_type¶
If you register an implementation as another type, the registration has to make sense.
import wireup
from wireup import injectable
class Cache: ...
@injectable(as_type=Cache)
class Mailer: ...
wireup.create_sync_container(injectables=[Mailer])
Why it fails:
- The registration says
Mailershould be used whereverCacheis requested, butMailerdoes not actually implementCache
Why this is bad:
- Code that expects a
Cachecould receive an object with the wrong behavior - The registration would make the type hints say one thing while the runtime object does another
Requests Made Later By Your Code¶
Some errors only happen when your code makes a dependency request Wireup could not have predicted during container creation or integration setup.
Examples:
- Calling
container.get(Foo)whenFoowas never registered - Calling
container.get(Cache, qualifier="analytics")when that qualifier does not exist - Asking a sync container to create an async-only dependency
These are not graph-validation failures. They depend on what your runtime code asks the container to do.
What Wireup Does Not Validate¶
Wireup validates the dependency graph. Runtime behavior inside your constructors and factories is outside Wireup's control.