Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
e85dd76
✨ Support multiple options with multiple values
nealian Sep 21, 2022
6c7a7e7
Merge remote-tracking branch 'upstream/master' into multiple_multiple…
nealian Nov 18, 2022
cbd07be
:rotating_light: Fix linting error
nealian Nov 21, 2022
ef0c536
Merge branch 'master' into multiple_multiple_options
tiangolo Dec 16, 2022
6303200
Merge branch 'tiangolo:master' into multiple_multiple_options
nealian Jan 2, 2023
dadcd13
Merge branch 'master' into multiple_multiple_options
nealian Feb 7, 2023
9c40d96
Merge branch 'master' into multiple_multiple_options
nealian Mar 8, 2023
d709166
Merge branch 'master' into multiple_multiple_options
nealian Nov 8, 2023
d1e1817
Merge branch 'master' into multiple_multiple_options
nealian Feb 2, 2024
0ac57c3
Merge branch 'master' into multiple_multiple_options
nealian Aug 16, 2024
f797a09
Fix tutorial docs, source, and tests to match current style
nealian Aug 16, 2024
c191eee
Add to per-file ignores, to match existing tutorial source
nealian Aug 16, 2024
fc2c70b
Merge branch 'master' into multiple_multiple_options
YuriiMotov Sep 19, 2025
e584685
🎨 [pre-commit.ci] Auto format from pre-commit.com hooks
pre-commit-ci[bot] Sep 19, 2025
db56796
Fix `is_tuple` and `is_list` conditions order
YuriiMotov Sep 19, 2025
979499d
Update code include format
YuriiMotov Sep 19, 2025
80cd4bf
Merge branch 'master' into multiple_multiple_options
svlandeg Nov 25, 2025
b381076
update tutorial files to use explicit Typer()
svlandeg Nov 25, 2025
87a0605
Merge branch 'master' into multiple_multiple_options
nealian Jan 4, 2026
8644bb0
🎨 Auto format
pre-commit-ci-lite[bot] Jan 4, 2026
955eb13
Add tutorial code for py310, and add, combine, and clean up tests.
nealian Jan 4, 2026
9d4da5c
🎨 Auto format
pre-commit-ci-lite[bot] Jan 4, 2026
f2d40bd
Forgot to import pytest oops
nealian Jan 4, 2026
c056d83
Linter doesn't understand
nealian Jan 4, 2026
0e9628f
Fix/limit lint scope for multiple_values python3.9
nealian Jan 5, 2026
be69233
Incorporate change suggestion from @YuriiMotov
nealian Jan 5, 2026
d8c639b
fix typo
nealian Jan 5, 2026
d1402a5
Fix docs highlighting
nealian Jan 5, 2026
9ac54fb
Mixed up my Python versions and their deprecations :sweat_smile:
nealian Jan 8, 2026
df934e8
🎨 Auto format
pre-commit-ci-lite[bot] Jan 8, 2026
fb72bad
Merge remote-tracking branch 'upstream/master' into multiple_multiple…
YuriiMotov Feb 17, 2026
c43d173
Update to Python 3.10 syntax
YuriiMotov Feb 17, 2026
aa2ffb7
Merge remote-tracking branch 'upstream/master' into multiple_multiple…
YuriiMotov Feb 18, 2026
e20d1bd
Fix code include
YuriiMotov Feb 18, 2026
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Multiple options with multiple values

For when simple doesn't quite cut it, you may also declare a *CLI option* that takes several values of different types and can be used multiple times.

For this, we use the standard Python `list` and declare it as a list of `tuple`:

{* docs_src/multiple_values/multiple_options_with_multiple_values/tutorial001_an_py310.py hl[9] *}

Just as before, the types internal to the `tuple` define the type of each value in the tuple.

## Check it

<div class="termy">

```console
$ python main.py

Congratulations, you're debt-free!

// Now let's borrow some money.
$ python main.py --borrow 2.5 Mark

Borrowed 2.50 from Mark

Total borrowed: 2.50

// And, of course, it may be used multiple times
$ python main.py --borrow 2.5 Mark --borrow 5.25 Sean --borrow 1.75 Wade

Borrowed 2.50 from Mark
Borrowed 5.25 from Sean
Borrowed 1.75 from Wade

Total borrowed: 9.50
```

</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from typing import Annotated

import typer

app = typer.Typer()


@app.command()
def main(borrow: Annotated[list[tuple[float, str]], typer.Option()] = []):
if not borrow:
print("Congratulations, you're debt-free!")
raise typer.Exit(0)
total = 0.0
for amount, person in borrow:
print(f"Borrowed {amount:.2f} from {person}")
total += amount
print()
print(f"Total borrowed: {total:.2f}")


if __name__ == "__main__":
app()
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import typer

app = typer.Typer()


@app.command()
def main(borrow: list[tuple[float, str]] = typer.Option([])):
if not borrow:
print("Congratulations, you're debt-free!")
raise typer.Exit(0)
total = 0.0
for amount, person in borrow:
print(f"Borrowed {amount:.2f} from {person}")
total += amount
print()
print(f"Total borrowed: {total:.2f}")


if __name__ == "__main__":
app()
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ nav:
- tutorial/multiple-values/index.md
- tutorial/multiple-values/multiple-options.md
- tutorial/multiple-values/options-with-multiple-values.md
- tutorial/multiple-values/multiple-options-with-multiple-values.md
- tutorial/multiple-values/arguments-with-multiple-values.md
- tutorial/prompt.md
- tutorial/progressbar.md
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,7 @@ ignore = [
# Default mutable data structure
"docs_src/options_autocompletion/tutorial006_an_py310.py" = ["B006"]
"docs_src/multiple_values/multiple_options/tutorial002_an_py310.py" = ["B006"]
"docs_src/multiple_values/multiple_options_with_multiple_values/tutorial001_an_py310.py" = ["B006"]
"docs_src/options_autocompletion/tutorial007_an_py310.py" = ["B006"]
"docs_src/options_autocompletion/tutorial008_an_py310.py" = ["B006"]
"docs_src/options_autocompletion/tutorial009_an_py310.py" = ["B006"]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import importlib
import subprocess
import sys
from types import ModuleType

import pytest
from typer.testing import CliRunner

runner = CliRunner()


@pytest.fixture(
name="mod",
params=[
pytest.param("tutorial001_py310"),
pytest.param("tutorial001_an_py310"),
],
)
def get_mod(request: pytest.FixtureRequest) -> ModuleType:
module_name = f"docs_src.multiple_values.multiple_options_with_multiple_values.{request.param}"
mod = importlib.import_module(module_name)
return mod


def test_main(mod: ModuleType):
result = runner.invoke(mod.app)
assert result.exit_code == 0
assert "Congratulations, you're debt-free!" in result.output


def test_borrow_1(mod: ModuleType):
result = runner.invoke(mod.app, ["--borrow", "2.5", "Mark"])
assert result.exit_code == 0
assert "Borrowed 2.50 from Mark" in result.output
assert "Total borrowed: 2.50" in result.output


def test_borrow_many(mod: ModuleType):
result = runner.invoke(
mod.app,
[
"--borrow",
"2.5",
"Mark",
"--borrow",
"5.25",
"Sean",
"--borrow",
"1.75",
"Wade",
],
)
assert result.exit_code == 0
assert "Borrowed 2.50 from Mark" in result.output
assert "Borrowed 5.25 from Sean" in result.output
assert "Borrowed 1.75 from Wade" in result.output
assert "Total borrowed: 9.50" in result.output


def test_invalid_borrow(mod: ModuleType):
result = runner.invoke(mod.app, ["--borrow", "2.5"])
assert result.exit_code != 0
# TODO: when deprecating Click 7, remove second option

assert (
"Option '--borrow' requires 2 arguments" in result.output
or "--borrow option requires 2 arguments" in result.output
)


def test_script(mod: ModuleType):
result = subprocess.run(
[sys.executable, "-m", "coverage", "run", mod.__file__, "--help"],
capture_output=True,
encoding="utf-8",
)
assert "Usage" in result.stdout
22 changes: 19 additions & 3 deletions typer/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -1671,10 +1671,24 @@ def get_click_param(
# Handle Tuples and Lists
if lenient_issubclass(origin, list):
main_type = get_args(main_type)[0]
assert not get_origin(main_type), (
"List types with complex sub-types are not currently supported"
)
list_origin = get_origin(main_type)
list_origin_is_tuple = lenient_issubclass(list_origin, tuple)
if not list_origin_is_tuple:
assert not list_origin, (
"List types with complex sub-types are not currently supported"
)
is_list = True
if list_origin_is_tuple:
types = []
for type_ in get_args(main_type):
assert not get_origin(type_), (
"List[Tuple] types with complex Tuple sub-types are not "
"currently supported"
)
types.append(
get_click_type(annotation=type_, parameter_info=parameter_info)
)
parameter_type = tuple(types)
elif lenient_issubclass(origin, tuple):
types = []
for type_ in get_args(main_type):
Expand All @@ -1692,6 +1706,8 @@ def get_click_param(
)
convertor = determine_type_convertor(main_type)
if is_list:
if list_origin_is_tuple:
convertor = generate_tuple_convertor(get_args(main_type))
convertor = generate_list_convertor(
convertor=convertor, default_value=default_value
)
Expand Down