Skip to main content

Parameter Metadata and Defaults

Typer relies on a sophisticated metadata system to bridge the gap between standard Python function signatures and the complex requirements of a Command Line Interface (CLI). This system ensures that Typer can distinguish between values that are intentionally missing, values set to None, and values inherited from different levels of the application hierarchy.

The Default Value Ambiguity

In Python, None is often used as a default value to indicate the absence of a parameter. However, in a CLI context, a user might explicitly want to pass None (or a null-equivalent) to an option. Furthermore, Typer supports a hierarchy where settings can be defined at the Typer app level, a callback level, or a specific command level.

To handle this, Typer introduces the DefaultPlaceholder class in typer/models.py:

class DefaultPlaceholder:
"""
You shouldn't use this class directly.

It's used internally to recognize when a default value has been overwritten, even
if the new value is `None`.
"""

def __init__(self, value: Any):
self.value = value

def __bool__(self) -> bool:
return bool(self.value)

By wrapping default values in this placeholder, Typer's internal logic can perform identity checks to determine if a value was explicitly provided by the developer. This is most visible in the "solving" logic within typer/main.py, such as solve_typer_info_help:

def solve_typer_info_help(typer_info: TyperInfo) -> str:
# Priority 1: Explicit value was set in app.add_typer()
if not isinstance(typer_info.help, DefaultPlaceholder):
return inspect.cleandoc(typer_info.help or "")

# ... logic continues to check callbacks and instances ...

# Value not set, use the default value inside the placeholder
return typer_info.help.value

This design allows for a priority system where an explicit value at the command level overrides a value at the app level, but if no value is provided at any level, the system falls back to the internal default stored within the placeholder.

Bridging Python Signatures with ParamMeta

When you define a Typer command, the library inspects the function signature to understand what parameters it expects. The ParamMeta class serves as an intermediate representation of a Python parameter during this inspection phase.

class ParamMeta:
empty = inspect.Parameter.empty

def __init__(
self,
*,
name: str,
default: Any = inspect.Parameter.empty,
annotation: Any = inspect.Parameter.empty,
) -> None:
self.name = name
self.default = default
self.annotation = annotation

Located in typer/models.py, ParamMeta captures three critical pieces of information:

  1. Name: The name of the parameter in the function.
  2. Default: The default value assigned in the function signature (which might be a ParameterInfo object like typer.Option(...)).
  3. Annotation: The type hint provided for the parameter.

Typer uses inspect.Parameter.empty to represent a parameter that has no default value or no type hint, maintaining compatibility with Python's standard inspection tools.

CLI Metadata with ParameterInfo

While ParamMeta describes the Python-side of a parameter, ParameterInfo describes the CLI-side. This class (and its common subclasses OptionInfo and ArgumentInfo) stores all the metadata required to generate a click.Option or click.Argument.

The constructor for ParameterInfo in typer/models.py is extensive, covering everything from help text and environment variables to custom parsers and Rich-formatting settings:

class ParameterInfo:
def __init__(
self,
*,
default: Any | None = None,
param_decls: Sequence[str] | None = None,
callback: Callable[..., Any] | None = None,
help: str | None = None,
# ... many other CLI-specific fields ...
):
# Validation: Ensure only one custom parsing method is used
if parser and click_type:
raise ValueError(
"Multiple custom type parsers provided. "
"`parser` and `click_type` may not both be provided."
)
self.default = default
# ... assignment of other fields ...

Constraints and Tradeoffs

The implementation of ParameterInfo reveals several design constraints:

  • Mutual Exclusivity of Parsers: As seen in the constructor, you cannot provide both a parser (a simple callable) and a click_type (a click.ParamType subclass). This prevents ambiguity in how the raw CLI string is converted into a Python object.
  • The Annotated Pattern: When using typing.Annotated, the ParameterInfo object is stored within the type metadata. However, Typer's logic in get_click_param (found in typer/main.py) must still resolve whether the parameter is a required argument or an optional flag based on whether a default value is present in the function signature.

The Resolution Lifecycle

The lifecycle of parameter metadata follows a specific path:

  1. Inspection: get_params_from_function (in typer/utils.py) uses inspect.signature to create a map of ParamMeta objects.
  2. Extraction: Typer looks at the default attribute of ParamMeta. If it's an instance of ParameterInfo, it extracts the CLI metadata. If it's a standard value, it treats it as the default value for an Option.
  3. Solving: The "solving" functions in typer/main.py resolve any DefaultPlaceholder values by checking the hierarchy of the application.
  4. Conversion: Finally, get_click_param takes the resolved metadata and the Python type annotation to produce a native Click object.

This layered approach allows Typer to provide a high-level, declarative API while maintaining the fine-grained control required by the underlying Click engine.