Problem
Context is already generic over three TypeVars — ConfigT, ApplicationStateT, ServicesT — giving consumers full type-safety for their config, application state, and service objects. However, user_config on Context is typed as object | None:
# context.py:63
user_config: object | None = None
And in profile.py, the UserConfigLoader protocol is:
def __call__(self, user_config: object | None) -> Path | None: ...
This means consumers who load typed user configuration (e.g., a dataclass with workspace paths, IDE preferences, etc.) have no way to express the type at the Context level — they must cast it in every command function:
def my_command(ctx: base_cli.Context) -> int:
user_cfg = cast(MyUserConfig, ctx.user_config) # repeated everywhere
...
Options
Option A — Add a fourth TypeVar UserConfigT
UserConfigT = TypeVar("UserConfigT")
@dataclass
class Context(Generic[ConfigT, ApplicationStateT, ServicesT, UserConfigT]):
...
user_config: UserConfigT | None = None
App would then wire UserConfigT from the CliProfile.load_user_config return type. This is consistent with how the other three generics work, but adds complexity to an already 4-parameter generic type.
Option B — Document user_config as intentionally opaque with a typed accessor pattern
Leave the field as object | None but document that consumers should access it through a typed property on a custom Context subclass or a typed helper. This keeps the Context signature simpler but provides no IDE support out of the box.
Option C — Use a Protocol-bounded TypeVar
Introduce a UserConfigProtocol that user config types can satisfy, and bound UserConfigT to it. Provides structural typing without requiring inheritance.
Recommendation
Option A gives the most consistent experience with how ConfigT, ApplicationStateT, and ServicesT already work. The downside (a 4-parameter generic) is mitigated by the fact that most consumers will use Context[dict, None, None, MyUserConfig] or a type alias.
Even if Option B or C is chosen, the current object | None annotation should have an explicit docstring note explaining the design choice and the recommended typed-access pattern.
Problem
Contextis already generic over three TypeVars —ConfigT,ApplicationStateT,ServicesT— giving consumers full type-safety for their config, application state, and service objects. However,user_configonContextis typed asobject | None:And in
profile.py, theUserConfigLoaderprotocol is:This means consumers who load typed user configuration (e.g., a dataclass with workspace paths, IDE preferences, etc.) have no way to express the type at the
Contextlevel — they must cast it in every command function:Options
Option A — Add a fourth TypeVar
UserConfigTAppwould then wireUserConfigTfrom theCliProfile.load_user_configreturn type. This is consistent with how the other three generics work, but adds complexity to an already 4-parameter generic type.Option B — Document
user_configas intentionally opaque with a typed accessor patternLeave the field as
object | Nonebut document that consumers should access it through a typed property on a customContextsubclass or a typed helper. This keeps theContextsignature simpler but provides no IDE support out of the box.Option C — Use a Protocol-bounded TypeVar
Introduce a
UserConfigProtocolthat user config types can satisfy, and boundUserConfigTto it. Provides structural typing without requiring inheritance.Recommendation
Option A gives the most consistent experience with how
ConfigT,ApplicationStateT, andServicesTalready work. The downside (a 4-parameter generic) is mitigated by the fact that most consumers will useContext[dict, None, None, MyUserConfig]or a type alias.Even if Option B or C is chosen, the current
object | Noneannotation should have an explicit docstring note explaining the design choice and the recommended typed-access pattern.