Configuring Execution State
To manage configuration and shared data across multiple commands in a Typer application, you can use a state object initialized within a callback. For advanced use cases involving the typer command-line tool itself, you can programmatically modify the internal execution state to control how applications are loaded.
Sharing State Across Commands
The most common way to manage execution state is to define a global object (like a dictionary or a custom class) and update it within an @app.callback(). This allows you to capture global options (like --verbose) and make them available to all subcommands.
import typer
app = typer.Typer()
# Initialize a simple state container
state = {"verbose": False}
@app.callback()
def main(verbose: bool = False):
"""
Main entry point that captures global configuration.
"""
if verbose:
print("Will write verbose output")
state["verbose"] = True
@app.command()
def delete(username: str):
if state["verbose"]:
print(f"Deleting user: {username}")
print(f"User {username} deleted")
if __name__ == "__main__":
app()
In this pattern, the main callback runs before any subcommand, ensuring the state dictionary is populated with the correct values from the command-line arguments.
Using a Custom State Class
For more complex applications, using a custom class or dataclass provides better structure and type safety than a dictionary.
import typer
from typing import Optional
class AppState:
def __init__(self):
self.verbose: bool = False
self.config_file: Optional[str] = None
state = AppState()
app = typer.Typer()
@app.callback()
def main(
verbose: bool = typer.Option(False, "--verbose", "-v"),
config: Optional[str] = typer.Option(None, "--config", "-c")
):
state.verbose = verbose
state.config_file = config
@app.command()
def sync():
if state.verbose:
print(f"Syncing using config: {state.config_file}")
# Sync logic here
Configuring CLI Tool State
The typer command-line tool (used for running scripts or generating documentation) uses a specific internal class typer.cli.State to track the target application. You can programmatically modify the global state instance in typer.cli to control which file, module, or app object the CLI tool interacts with.
This is particularly useful when building tools that wrap or extend the Typer CLI's functionality, such as custom documentation generators.
from pathlib import Path
from typer.cli import state, get_typer_from_state
# Programmatically configure the CLI execution state
# This tells the Typer CLI logic where to find the app
state.file = Path("main.py")
state.app = "app" # The name of the Typer() instance in main.py
# Use the internal helper to load the Typer object based on the state
typer_app = get_typer_from_state()
if typer_app:
print(f"Successfully loaded app: {state.app} from {state.file}")
State Properties
The typer.cli.State class (defined in typer/cli.py) supports the following attributes:
file: Apathlib.Pathobject pointing to the Python script.module: A string representing the Python module name (used iffileis not set).app: A string representing the name of thetyper.Typerinstance variable.func: A string representing the name of a function to be treated as the entry point.
Automatic State Updates
In the standard Typer CLI flow, the state is automatically populated from Click context parameters using the maybe_update_state function. This function resolves whether a provided argument is a file path or a module name:
# Internal logic from typer/cli.py
def maybe_update_state(ctx: click.Context) -> None:
path_or_module = ctx.params.get("path_or_module")
if path_or_module:
file_path = Path(path_or_module)
if file_path.exists() and file_path.is_file():
state.file = file_path
else:
# Logic to treat as module...
state.module = path_or_module
Troubleshooting State Conflicts
- Internal vs. External State: Do not confuse
typer.cli.Statewith your own application state.typer.cli.Stateis strictly for thetypercommand-line tool's internal discovery mechanism. For your own app's logic, always define a separate state container. - Initialization Order: Ensure your state is modified within the
@app.callback(). If you try to access state values in a subcommand that were supposed to be set in a callback, but the callback hasn't run (e.g., during unit testing without proper context), you may encounter default or uninitialized values. - CLI Discovery Priority: When using
typer.cli.State, if bothfileandmoduleare set, the discovery logic inget_typer_from_stateprioritizes thefilepath if it exists on disk.