Troubleshooting¶
This guide uses a symptom-first approach: find the problem you're experiencing and follow the steps to fix it.
For a complete list of every error message produced by pico-ioc, see the Error Reference in the FAQ.
If you're using pico-boot, pico-fastapi, or pico-pydantic, see also the unified pico-boot troubleshooting guide.
"My component is not found" (ProviderNotFoundError)¶
Step 1: Is the class decorated with @component?¶
Step 2: Is the module included in init()?¶
Common mistake: listing the package (
"myapp") instead of the module.init(modules=["myapp"])only scansmyapp/__init__.py. IfDatabaselives inmyapp/database.py, list it explicitly or re-export from__init__.py.
Step 3: Is it a third-party class you can't decorate?¶
Use @provides instead:
from pico_ioc import provides
@provides(redis.Redis)
def build_redis(config: RedisConfig) -> redis.Redis:
return redis.Redis.from_url(config.URL)
Step 4: Is there a typo in the type hint?¶
The type hint must exactly match the registered class:
Step 5: Is the component conditionally excluded?¶
If using profiles, verify the component's conditional_profiles matches:
@component(conditional_profiles=("prod",))
class ProdOnlyService: ...
# This won't find ProdOnlyService:
container = init(modules=["myapp"], profiles=("dev",))
"Circular dependency detected" (InvalidBindingError)¶
InvalidBindingError: Invalid bindings:
- Circular dependency detected: ServiceA -> ServiceB -> ServiceA
Option 1: Use lazy=True on one side¶
The lazy component receives a proxy that resolves on first attribute access, breaking the initialization cycle.
Option 2: Use an event to decouple¶
If the dependency is only needed for notifications, replace the direct dependency with an event:
from pico_ioc import component, subscribe, Event
class UserCreated(Event):
user_id: int
@component
class ServiceA:
def __init__(self, bus: EventBus):
self.bus = bus
async def create_user(self):
# ... create user ...
await self.bus.publish(UserCreated(user_id=42))
@component
class ServiceB:
@subscribe(UserCreated)
async def on_user_created(self, event: UserCreated):
# ... react to event ...
pass
Option 3: Restructure the dependency¶
Often a circular dependency signals a missing abstraction. Extract the shared logic into a third component that both sides depend on.
"Synchronous get() received an awaitable" (AsyncResolutionError)¶
Cause¶
You called container.get() on a component that has async initialization (__ainit__, async @configure, or an async @provides factory).
Fix 1: Use aget()¶
Fix 2: Mark the component as lazy=True¶
@component(lazy=True)
class AsyncService:
async def __ainit__(self):
self.conn = await create_connection()
Lazy components return a proxy from get() that defers async initialization until the first attribute access in an async context.
Decision matrix¶
| Scenario | Use |
|---|---|
| You're in an async context (e.g., FastAPI handler) | await container.aget(X) |
| You need the component in a sync context | @component(lazy=True) |
| You're using pico-fastapi | Automatic -- controllers use aget() internally |
"Scope 'request' is not active" (ScopeError)¶
Cause¶
You called container.get(RequestScopedComponent) outside of a scope context.
Fix¶
Wrap the resolution in the appropriate scope:
With pico-fastapi, this is handled automatically by PicoScopeMiddleware. If you see this error with pico-fastapi, check that:
- Your middleware priority is correct (negative = outer, positive = inner)
- You're not resolving request-scoped components in startup events or background tasks
"Unknown scope" (ScopeError)¶
Cause¶
A component uses scope="tenant", but the scope was not registered.
Fix¶
Pass custom_scopes to init():
See How to Create Custom Scopes for a full example.
"Missing configuration key" (ConfigurationError)¶
Cause¶
A @configured dataclass has a required field with no default, and no configuration source provides a value for that key.
Fix¶
Either provide the key in your configuration source:
Or add a default value:
Configuration precedence¶
Later sources override earlier ones:
- Default values in the dataclass
- YAML/JSON files (in the order specified)
- Environment variables (highest priority)
See Configuration Binding for the full precedence rules.
"Async interceptor returned awaitable on sync method"¶
Cause¶
An interceptor's invoke() method returned a coroutine, but the intercepted method is synchronous.
Fix¶
Ensure your interceptor handles both sync and async methods:
class MyInterceptor(MethodInterceptor):
def invoke(self, ctx: MethodCtx):
# For sync methods, don't await
result = ctx.proceed()
return result
async def ainvoke(self, ctx: MethodCtx):
# For async methods
result = await ctx.proceed()
return result
Or only apply the interceptor to async methods.
"My override is not working in tests"¶
Common pitfall: overriding with a class instead of an instance¶
# Wrong - passes the class object itself
overrides={Database: FakeDatabase}
# Correct - passes an instance
overrides={Database: FakeDatabase()}
Common pitfall: overriding the concrete type instead of the interface¶
# If consumers depend on the Protocol:
class UserService:
def __init__(self, db: Database): ... # Database is a Protocol
# Override by the Protocol, not the concrete class:
overrides={Database: FakeDatabase()} # correct
overrides={PostgresDatabase: FakeDatabase()} # won't affect UserService
Common pitfall: shared state between tests¶
Singletons persist across the container's lifetime. Create a fresh container per test:
@pytest.fixture
def container():
c = init(modules=["myapp"], overrides={Database: FakeDatabase()})
yield c
c.shutdown()
Debugging tips¶
Enable debug logging¶
Inspect registered components¶
container = init(modules=["myapp"])
stats = container.stats()
print(f"Registered: {stats['registered_components']}")
Export the dependency graph¶
See also¶
- Error Reference -- every error message with cause and fix
- Unified pico-boot troubleshooting guide -- covers pico-boot, pico-fastapi, and pico-pydantic
- FAQ -- common questions
- Testing guide -- testing patterns