Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
a6a797d
Implement list parsing from string with separators.
libklein Apr 17, 2024
7eb3fd2
🎨 [pre-commit.ci] Auto format from pre-commit.com hooks
pre-commit-ci[bot] Apr 17, 2024
cec52c9
Do not allow multi-character separators for List[T] parsing.
libklein Apr 17, 2024
f1ca3bd
Move multiple separator error checking to option construction.
libklein Apr 18, 2024
aca33a5
🎨 [pre-commit.ci] Auto format from pre-commit.com hooks
pre-commit-ci[bot] Apr 18, 2024
46b4690
Fix linter errors.
libklein Apr 18, 2024
7c78c51
Ignore mutable data structures error for added tutorial file.
libklein Apr 18, 2024
c0aba28
🎨 [pre-commit.ci] Auto format from pre-commit.com hooks
pre-commit-ci[bot] Apr 18, 2024
6233289
Use typing.List instead of list in test_others.
libklein Apr 18, 2024
f09da73
Do not collect coverage on unused command in test_others.
libklein Apr 18, 2024
3cdd682
Add documentation.
libklein Apr 18, 2024
03558cb
Rename `multiple_separator` to `separator`.
libklein Apr 25, 2024
e54193b
Fix missing word in documentation
libklein Apr 26, 2024
e4781dd
update documentation
svlandeg Jul 24, 2024
1793f0f
Extend error message to point to valid separators
svlandeg Jul 24, 2024
ae11002
Update error msg in test
svlandeg Jul 24, 2024
de078a6
Merge branch 'master' into multiple-arguments-via-separated-lists
svlandeg Aug 9, 2024
d5328fa
Update tests to clarify that whitespace between options is unsupported.
libklein Nov 30, 2024
0eb3fe5
Merge branch 'master' into multiple-arguments-via-separated-lists
svlandeg Aug 28, 2025
2afc749
update docs to new format
svlandeg Aug 28, 2025
60108fe
fix
svlandeg Aug 28, 2025
1277d50
Merge branch 'master' into multiple-arguments-via-separated-lists
svlandeg Sep 1, 2025
9c313dd
Merge branch 'master' into multiple-arguments-via-separated-lists
svlandeg Oct 21, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions docs/tutorial/multiple-values/multiple-options.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,49 @@ The sum is 9.5
```

</div>

## Passing multiple values in a single argument

**Typer** supports passing multiple arguments with a single option, by using the `separator` parameter in combination with `typing.List[T]` types.
This feature makes it easy to parse multiple values from a single command-line argument into a list in your application.

To use this feature, define a command-line option that accepts multiple values separated by a specific character (such as a comma). Here's an example of how to implement this:

{* docs_src/multiple_values/multiple_options/tutorial003_an.py hl[7] *}

Check it:

<div class="termy">

```console
// With no optional CLI argument
$ python main.py

The sum is 0

// With one number argument
$ python main.py --number 2

The sum is 2.0

// With several number arguments, split using the separator defined by the Option argument
$ python main.py --number "2, 3, 4.5"

The sum is 9.5

// You can remove the quotes if no whitespace is added between the numbers
$ python main.py --number 2,3,4.5

The sum is 9.5

// Supports passing the option multiple times. This joins all values to a single list
$ python main.py --number 2,3,4.5 --number 5

The sum is 14.5
```

</div>

/// warning

Only single-character non-whitespace separators are supported.
11 changes: 11 additions & 0 deletions docs_src/multiple_values/multiple_options/tutorial003.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from typing import List

import typer


def main(number: List[float] = typer.Option([], separator=",")):
print(f"The sum is {sum(number)}")


if __name__ == "__main__":
typer.run(main)
12 changes: 12 additions & 0 deletions docs_src/multiple_values/multiple_options/tutorial003_an.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from typing import List

import typer
from typing_extensions import Annotated


def main(number: Annotated[List[float], typer.Option(separator=",")] = []):
print(f"The sum is {sum(number)}")


if __name__ == "__main__":
typer.run(main)
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@ ignore = [
# Default mutable data structure
"docs_src/options_autocompletion/tutorial006_an.py" = ["B006"]
"docs_src/multiple_values/multiple_options/tutorial002_an.py" = ["B006"]
"docs_src/multiple_values/multiple_options/tutorial003_an.py" = ["B006"]
"docs_src/options_autocompletion/tutorial007_an.py" = ["B006"]
"docs_src/options_autocompletion/tutorial008_an.py" = ["B006"]
"docs_src/options_autocompletion/tutorial009_an.py" = ["B006"]
Expand Down
30 changes: 30 additions & 0 deletions tests/test_others.py
Original file line number Diff line number Diff line change
Expand Up @@ -321,3 +321,33 @@ def test_split_opt():
prefix, opt = _split_opt("verbose")
assert prefix == ""
assert opt == "verbose"


def test_multiple_options_separator_1_unsupported_separator():
app = typer.Typer()

@app.command()
def main(names: typing.List[str] = typer.Option(..., separator="\t \n")):
pass # pragma: no cover

with pytest.raises(typer.UnsupportedSeparatorError) as exc_info:
runner.invoke(app, [])
assert (
str(exc_info.value)
== "Error in definition of Option 'names'. Only single-character non-whitespace separators are supported, but got \"\t \n\"."
)


def test_multiple_options_separator_2_non_list_type():
app = typer.Typer()

@app.command()
def main(names: str = typer.Option(..., separator=",")):
pass # pragma: no cover

with pytest.raises(typer.SeparatorForNonListTypeError) as exc_info:
runner.invoke(app, [])
assert (
str(exc_info.value)
== "Multiple values are supported for List[T] types only. Annotate 'names' as List[str] to support multiple values."
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import subprocess
import sys

import typer
from typer.testing import CliRunner

from docs_src.multiple_values.multiple_options import tutorial003 as mod

runner = CliRunner()
app = typer.Typer()
app.command()(mod.main)


def test_main():
result = runner.invoke(app)
assert result.exit_code == 0
assert "The sum is 0" in result.output


def test_1_number():
result = runner.invoke(app, ["--number", "2"])
assert result.exit_code == 0
assert "The sum is 2.0" in result.output


def test_2_number():
result = runner.invoke(app, ["--number", "2,3,4.5"], catch_exceptions=False)
assert result.exit_code == 0
assert "The sum is 9.5" in result.output


def test_3_number():
result = runner.invoke(app, ["--number", "2,3,4.5", "--number", "5"])
assert result.exit_code == 0
assert "The sum is 14.5" in result.output


def test_script():
result = subprocess.run(
[sys.executable, "-m", "coverage", "run", mod.__file__, "--help"],
capture_output=True,
encoding="utf-8",
)
assert "Usage" in result.stdout
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import subprocess
import sys

import typer
from typer.testing import CliRunner

from docs_src.multiple_values.multiple_options import tutorial003 as mod

runner = CliRunner()
app = typer.Typer()
app.command()(mod.main)


def test_main():
result = runner.invoke(app)
assert result.exit_code == 0
assert "The sum is 0" in result.output


def test_1_number():
result = runner.invoke(app, ["--number", "2"])
assert result.exit_code == 0
assert "The sum is 2.0" in result.output


def test_2_number():
result = runner.invoke(app, ["--number", "2,3,4.5"])
assert result.exit_code == 0
assert "The sum is 9.5" in result.output


def test_3_number():
result = runner.invoke(app, ["--number", "2,3,4.5", "--number", "5"])
assert result.exit_code == 0
assert "The sum is 14.5" in result.output


def test_script():
result = subprocess.run(
[sys.executable, "-m", "coverage", "run", mod.__file__, "--help"],
capture_output=True,
encoding="utf-8",
)
assert "Usage" in result.stdout
6 changes: 6 additions & 0 deletions typer/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,9 @@
from .models import FileTextWrite as FileTextWrite
from .params import Argument as Argument
from .params import Option as Option
from .utils import (
SeparatorForNonListTypeError as SeparatorForNonListTypeError,
)
from .utils import (
UnsupportedSeparatorError as UnsupportedSeparatorError,
)
16 changes: 16 additions & 0 deletions typer/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import inspect
import os
import sys
import typing as t
from difflib import get_close_matches
from enum import Enum
from gettext import gettext as _
Expand All @@ -26,6 +27,7 @@
import click.shell_completion
import click.types
import click.utils
from click import Context

from ._typing import Literal

Expand Down Expand Up @@ -446,6 +448,7 @@ def __init__(
show_envvar: bool = False,
# Rich settings
rich_help_panel: Union[str, None] = None,
separator: Optional[str] = None,
):
super().__init__(
param_decls=param_decls,
Expand Down Expand Up @@ -475,6 +478,19 @@ def __init__(
)
_typer_param_setup_autocompletion_compat(self, autocompletion=autocompletion)
self.rich_help_panel = rich_help_panel
self.original_type = type
self.separator = separator

def _parse_separated_parameter_list(self, parameter_values: List[str]) -> List[str]:
values = []
for param_str_list in parameter_values:
values.extend(param_str_list.split(self.separator))
return values

def process_value(self, ctx: Context, value: t.Any) -> t.Any:
if self.separator is not None:
value = self._parse_separated_parameter_list(value)
return super().process_value(ctx, value)

def _get_default_string(
self,
Expand Down
19 changes: 18 additions & 1 deletion typer/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,11 @@
TyperInfo,
TyperPath,
)
from .utils import get_params_from_function
from .utils import (
SeparatorForNonListTypeError,
UnsupportedSeparatorError,
get_params_from_function,
)

_original_except_hook = sys.excepthook
_typer_developer_exception_attr_name = "__typer_developer_exception__"
Expand Down Expand Up @@ -894,6 +898,18 @@ def get_click_param(
param_decls.extend(parameter_info.param_decls)
else:
param_decls.append(default_option_declaration)

# Check the multiple separator option for validity
separator = None
if parameter_info.separator:
separator = parameter_info.separator.strip()

if not is_list:
raise SeparatorForNonListTypeError(param.name, main_type)

if len(separator) != 1:
raise UnsupportedSeparatorError(param.name, parameter_info.separator)

return (
TyperOption(
# Option
Expand Down Expand Up @@ -926,6 +942,7 @@ def get_click_param(
autocompletion=get_param_completion(parameter_info.autocompletion),
# Rich settings
rich_help_panel=parameter_info.rich_help_panel,
separator=separator,
),
convertor,
)
Expand Down
2 changes: 2 additions & 0 deletions typer/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,7 @@ def __init__(
path_type: Union[None, Type[str], Type[bytes]] = None,
# Rich settings
rich_help_panel: Union[str, None] = None,
separator: Optional[str] = None,
):
super().__init__(
default=default,
Expand Down Expand Up @@ -398,6 +399,7 @@ def __init__(
self.hide_input = hide_input
self.count = count
self.allow_from_autoenv = allow_from_autoenv
self.separator = separator


class ArgumentInfo(ParameterInfo):
Expand Down
3 changes: 3 additions & 0 deletions typer/params.py
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,8 @@ def Option(
path_type: Union[None, Type[str], Type[bytes]] = None,
# Rich settings
rich_help_panel: Union[str, None] = None,
# Multiple values
separator: Optional[str] = None,
) -> Any:
return OptionInfo(
# Parameter
Expand Down Expand Up @@ -257,6 +259,7 @@ def Option(
path_type=path_type,
# Rich settings
rich_help_panel=rich_help_panel,
separator=separator,
)


Expand Down
27 changes: 27 additions & 0 deletions typer/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,3 +188,30 @@ def get_params_from_function(func: Callable[..., Any]) -> Dict[str, ParamMeta]:
name=param.name, default=default, annotation=annotation
)
return params


class SeparatorForNonListTypeError(Exception):
argument_name: str
argument_type: Type[Any]

def __init__(self, argument_name: str, argument_type: Type[Any]):
self.argument_name = argument_name
self.argument_type = argument_type

def __str__(self) -> str:
return f"Multiple values are supported for List[T] types only. Annotate {self.argument_name!r} as List[{self.argument_type.__name__}] to support multiple values."


class UnsupportedSeparatorError(Exception):
argument_name: str
separator: str

def __init__(self, argument_name: str, separator: str):
self.argument_name = argument_name
self.separator = separator

def __str__(self) -> str:
return (
f"Error in definition of Option {self.argument_name!r}. "
f'Only single-character non-whitespace separators are supported, but got "{self.separator}".'
)
Loading