In some occasions you might want to have some custom logic for a specific CLI parameter (for a CLI option or CLI argument) that is executed with the value received from the terminal.
In those cases you can use a CLI parameter callback function.
For example, you could do some validation before the rest of the code is executed.
fromtypingimportOptionalimporttyperfromtyping_extensionsimportAnnotateddefname_callback(value:str):ifvalue!="Camila":raisetyper.BadParameter("Only Camila is allowed")returnvaluedefmain(name:Annotated[Optional[str],typer.Option(callback=name_callback)]=None):print(f"Hello {name}")if__name__=="__main__":typer.run(main)
Tip
Prefer to use the Annotated version if possible.
fromtypingimportOptionalimporttyperdefname_callback(value:str):ifvalue!="Camila":raisetyper.BadParameter("Only Camila is allowed")returnvaluedefmain(name:Optional[str]=typer.Option(default=None,callback=name_callback)):print(f"Hello {name}")if__name__=="__main__":typer.run(main)
Here you pass a function to typer.Option() or typer.Argument() with the keyword argument callback.
The function receives the value from the command line. It can do anything with it, and then return the value.
In this case, if the --name is not Camila we raise a typer.BadParameter() exception.
The BadParameter exception is special, it shows the error with the parameter that generated it.
There's something to be aware of with callbacks and completion that requires some small special handling.
But first let's just use completion in your shell (Bash, Zsh, Fish, or PowerShell).
After installing completion (for your own Python package), when you use your CLI program and start adding a CLI option with -- an then hit TAB, your shell will show you the available CLI options (the same for CLI arguments, etc).
To check it quickly with the previous script use the typer command:
fast →💬 Hit the TAB key in your keyboard below where you see the: [TAB]typer ./main.py [TAB][TAB] 💬 Depending on your terminal/shell you will get some completion like this ✨run -- Run the provided Typer app. utils -- Extra utility commands for Typer apps.
💬 Then try with "run" and --helptyper ./main.py run --help 💬 You get a help text with your CLI options as you normally wouldUsage: typer run [OPTIONS]
Run the provided Typer app.
Options: --name TEXT [required] --help Show this message and exit.
💬 Then try completion with your programtyper ./main.py run --[TAB][TAB] 💬 You get completion for CLI options--help -- Show this message and exit. --name
💬 And you can run it as if it was with Python directlytyper ./main.py run --name Camila Hello Camila
The way it works internally is that the shell/terminal will call your CLI program with some special environment variables (that hold the current CLI parameters, etc) and your CLI program will print some special values that the shell will use to present completion. All this is handled for you by Typer behind the scenes.
But the main important point is that it is all based on values printed by your program that the shell reads.
Let's say that when the callback is running, we want to show a message saying that it's validating the name:
fromtypingimportOptionalimporttyperfromtyping_extensionsimportAnnotateddefname_callback(value:str):print("Validating name")ifvalue!="Camila":raisetyper.BadParameter("Only Camila is allowed")returnvaluedefmain(name:Annotated[Optional[str],typer.Option(callback=name_callback)]=None):print(f"Hello {name}")if__name__=="__main__":typer.run(main)
Tip
Prefer to use the Annotated version if possible.
fromtypingimportOptionalimporttyperdefname_callback(value:str):print("Validating name")ifvalue!="Camila":raisetyper.BadParameter("Only Camila is allowed")returnvaluedefmain(name:Optional[str]=typer.Option(default=None,callback=name_callback)):print(f"Hello {name}")if__name__=="__main__":typer.run(main)
And because the callback will be called when the shell calls your program asking for completion, that message "Validating name" will be printed and it will break completion.
It will look something like:
fast →💬 Run it normallytyper ./main.py run --name Camila 💬 See the extra message "Validating name"Validating name Hello Camila
typer ./main.py run --[TAB][TAB] 💬 Some weird broken error message ⛔️(eval):1: command not found: Validating rutyper ./main.pyed Typer app.
The same way you can access the typer.Context by declaring a function parameter with its value, you can declare another function parameter with type typer.CallbackParam to get the specific Click Parameter object.
fromtypingimportOptionalimporttyperfromtyping_extensionsimportAnnotateddefname_callback(ctx:typer.Context,param:typer.CallbackParam,value:str):ifctx.resilient_parsing:returnprint(f"Validating param: {param.name}")ifvalue!="Camila":raisetyper.BadParameter("Only Camila is allowed")returnvaluedefmain(name:Annotated[Optional[str],typer.Option(callback=name_callback)]=None):print(f"Hello {name}")if__name__=="__main__":typer.run(main)
Tip
Prefer to use the Annotated version if possible.
fromtypingimportOptionalimporttyperdefname_callback(ctx:typer.Context,param:typer.CallbackParam,value:str):ifctx.resilient_parsing:returnprint(f"Validating param: {param.name}")ifvalue!="Camila":raisetyper.BadParameter("Only Camila is allowed")returnvaluedefmain(name:Optional[str]=typer.Option(default=None,callback=name_callback)):print(f"Hello {name}")if__name__=="__main__":typer.run(main)
It's probably not very common, but you could do it if you need it.
For example if you had a callback that could be used by several CLI parameters, that way the callback could know which parameter is each time.
Check it:
fast →python main.py --name Camila Validating param: name Hello Camila
Because you get the relevant data in the callback function based on standard Python type annotations, you get type checks and autocompletion in your editor for free.
And Typer will make sure you get the function parameters you want.
You don't have to worry about their names, their order, etc.
As it's based on standard Python types, it "just works". ✨
The value function parameter in the callback can also have any name (e.g. lastname) and any type, but it should have the same type annotation as in the main function, because that's what it will receive.
It's also possible to not declare its type. It will still work.
And it's possible to not declare the value parameter at all, and, for example, only get the typer.Context. That will also work.