From 6e051d8c34cb62e4b5e6fb21d0449d55341455fa Mon Sep 17 00:00:00 2001 From: Ananya Date: Wed, 4 Feb 2026 16:23:19 +0530 Subject: [PATCH 1/3] docs: clarify usage of option with multiple values --- .../options_with_multiple_values/tutorial001_py39.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs_src/multiple_values/options_with_multiple_values/tutorial001_py39.py b/docs_src/multiple_values/options_with_multiple_values/tutorial001_py39.py index f21e794102..37aa212f5c 100644 --- a/docs_src/multiple_values/options_with_multiple_values/tutorial001_py39.py +++ b/docs_src/multiple_values/options_with_multiple_values/tutorial001_py39.py @@ -1,3 +1,15 @@ +""" +This example shows how to define a Typer option that accepts multiple values +using a tuple type hint. + +The --user option expects exactly three values, in this order: +1. username (str) +2. coins (int) +3. is_wizard (bool) + +Example usage: +python tutorial001_py39.py --user Harry 100 true +""" import typer app = typer.Typer() From bc3938cfd7e1ab9f9d2e2c568ade5c2685e60c7b Mon Sep 17 00:00:00 2001 From: ananya4khandelwal Date: Sun, 8 Feb 2026 18:20:21 +0530 Subject: [PATCH 2/3] test: Add failing tests exposing tuple/list handling bugs --- tests/test_typer_bugs.py | 288 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 288 insertions(+) create mode 100644 tests/test_typer_bugs.py diff --git a/tests/test_typer_bugs.py b/tests/test_typer_bugs.py new file mode 100644 index 0000000000..e194ccf98c --- /dev/null +++ b/tests/test_typer_bugs.py @@ -0,0 +1,288 @@ +""" +Failing tests that expose bugs in Typer's multiple values handling. + +These tests document known issues and inconsistencies that should be fixed. +Related GitHub issues: +- Tuple vs List inconsistency: https://github.com/fastapi/typer/issues/[TBD] +- Multiple value option returns tuple not list: https://github.com/fastapi/typer/issues/[TBD] + +Author: Kaushaki Khandelwal +Date: 2026-02-08 +""" + +import pytest +from typer.testing import CliRunner +import typer +from typing import List + +runner = CliRunner() + + +class TestTupleOptionNoneDefaults: + """ + BUG: Tuple options with (None, None, None) defaults don't properly validate. + + The issue: When a tuple option has None values in its default tuple, + the validation and type conversion doesn't handle missing values correctly. + """ + + def test_tuple_option_none_default_should_handle_missing_gracefully(self): + """ + EXPECTED: Should allow command to run without the --user flag + ACTUAL: May raise validation error or handle None incorrectly + + Related to: tutorial001_py39.py + """ + app = typer.Typer() + + @app.command() + def main(user: tuple[str, int, bool] = typer.Option((None, None, None))): + username, coins, is_wizard = user + if not username: + typer.echo("No user provided") + raise typer.Abort() + typer.echo(f"The username {username} has {coins} coins") + if is_wizard: + typer.echo("And this user is a wizard!") + + # This test currently FAILS or behaves inconsistently + result = runner.invoke(app, []) + + # We expect graceful handling of missing values + assert result.exit_code == 1 # Should abort gracefully + assert "No user provided" in result.output + + def test_tuple_option_partial_values_should_error(self): + """ + BUG: Providing partial tuple values should give clear error + + EXPECTED: Clear error message about needing all 3 values + ACTUAL: May accept partial values or give confusing error + """ + app = typer.Typer() + + @app.command() + def main(user: tuple[str, int, bool] = typer.Option((None, None, None))): + username, coins, is_wizard = user + typer.echo(f"User: {username}") + + # Providing only 2 values when 3 are expected + result = runner.invoke(app, ["--user", "Harry", "100"]) + + # Should fail with clear error + assert result.exit_code != 0 + assert "user" in result.output.lower() or "values" in result.output.lower() + + +class TestListVsTupleInconsistency: + """ + BUG: Multiple value options return tuple instead of list despite type hint + + GitHub Issue: Multiple value option gives tuple and not list + Reference: https://github.com/fastapi/typer/issues/[issue_number] + + The documentation states that List[str] should receive values as a list, + but the actual behavior returns a tuple. + """ + + @pytest.mark.xfail( + reason="Known bug: multiple value option returns tuple instead of list" + ) + def test_list_option_should_return_list_not_tuple(self): + """ + EXPECTED: List[str] type hint should receive actual list + ACTUAL: Receives tuple instead + + This contradicts the documentation and type hints. + """ + app = typer.Typer() + + received_type = None + + @app.command() + def main(names: List[str] = typer.Option([], "--name", multiple=True)): + nonlocal received_type + received_type = type(names) + typer.echo(f"Type: {type(names).__name__}") + typer.echo(f"Names: {names}") + + result = runner.invoke(app, ["--name", "Alice", "--name", "Bob"]) + + assert result.exit_code == 0 + # This assertion FAILS - we get tuple instead of list + assert received_type == list, f"Expected list but got {received_type}" + assert isinstance(result.output, str) + + @pytest.mark.xfail(reason="Known bug: list operations fail on tuple return") + def test_list_type_hint_should_support_list_operations(self): + """ + EXPECTED: Should be able to use list methods like .append() + ACTUAL: Fails because it's actually a tuple + """ + app = typer.Typer() + + @app.command() + def main(items: List[str] = typer.Option([], multiple=True)): + # Type hint says List, so we should be able to use list methods + try: + items.append("new_item") # This should work if it's a list + typer.echo("Success: can use list methods") + except AttributeError: + typer.echo("FAIL: cannot use list methods - it's a tuple!") + + result = runner.invoke(app, ["--items", "a", "--items", "b"]) + + # This test currently FAILS + assert "Success" in result.output + assert "FAIL" not in result.output + + +class TestEmptyDefaultBehavior: + """ + BUG: Empty defaults behave inconsistently between tuple and list options + """ + + def test_empty_tuple_default_behavior(self): + """ + Document how empty tuple defaults currently behave + """ + app = typer.Typer() + + @app.command() + def main(values: tuple[str, str] = typer.Option(("", ""))): + typer.echo(f"Values: {values}") + + # How should this behave when user provides nothing? + result = runner.invoke(app, []) + + # Current behavior might be inconsistent + # This test documents what happens + assert result.exit_code == 0 + + def test_none_vs_empty_string_in_tuple(self): + """ + BUG: Inconsistent handling of None vs empty string in defaults + """ + app = typer.Typer() + + @app.command() + def main( + user1: tuple[str, int] = typer.Option((None, None)), + user2: tuple[str, int] = typer.Option(("", 0)), + ): + typer.echo(f"User1: {user1}") + typer.echo(f"User2: {user2}") + + result = runner.invoke(app, []) + + # Should these behave the same? Currently they might not + assert result.exit_code == 0 + + +class TestTupleTypeConversion: + """ + BUG: Type conversion in tuples may not handle all cases correctly + """ + + def test_tuple_bool_conversion_edge_cases(self): + """ + Test various bool representations in tuple options + """ + app = typer.Typer() + + @app.command() + def main(user: tuple[str, int, bool] = typer.Option((None, None, None))): + username, coins, is_wizard = user + typer.echo(f"{username}: {coins} coins, wizard={is_wizard}") + + # Test various bool representations + test_cases = [ + (["--user", "Harry", "100", "true"], True), + (["--user", "Ron", "50", "false"], False), + (["--user", "Hermione", "200", "1"], True), + (["--user", "Draco", "150", "0"], False), + ] + + for args, expected_bool in test_cases: + result = runner.invoke(app, args) + # Document current behavior - may have inconsistencies + print(f"Args: {args}") + print(f"Output: {result.output}") + print(f"Exit code: {result.exit_code}") + + +# Mark entire class for investigation +@pytest.mark.skip(reason="Tests need Typer to be installed in development mode") +class TestRequiresDevInstall: + """ + These tests require Typer to be installed in development mode. + + To run: pip install -e . + """ + + def test_with_actual_tutorial_file(self): + """Test the actual tutorial file""" + from docs_src.multiple_values.options_with_multiple_values import tutorial001_py39 + + app = tutorial001_py39.app + result = runner.invoke(app, []) + + assert result.exit_code == 1 + assert "No user provided" in result.output + + +# Additional edge case tests +class TestEdgeCases: + """ + Additional edge cases that might expose bugs + """ + + def test_mixing_required_and_optional_tuple_options(self): + """ + How does Typer handle mix of required and optional tuple options? + """ + app = typer.Typer() + + @app.command() + def main( + required: tuple[str, int], # Required + optional: tuple[str, int] = typer.Option((None, None)), # Optional + ): + typer.echo(f"Required: {required}, Optional: {optional}") + + # This might have unexpected behavior + result = runner.invoke(app, ["value", "42"]) + + # Document the actual behavior + print(f"Exit code: {result.exit_code}") + print(f"Output: {result.output}") + + @pytest.mark.xfail(reason="Investigating behavior with nested types") + def test_list_of_tuples_not_supported(self): + """ + BUG: List[Tuple[str, str]] should work but gives AssertionError + + Reference: GitHub discussions about List[Tuple[str, str]] support + """ + app = typer.Typer() + + with pytest.raises(AssertionError, match="complex sub-types"): + @app.command() + def main( + pairs: List[tuple[str, str]] = typer.Option([]) + ): + typer.echo(f"Pairs: {pairs}") + + # This definition itself should raise an error + # "List types with complex sub-types are not currently supported" + + +if __name__ == "__main__": + # Run the tests to see which ones fail + print("=" * 60) + print("TYPER BUG DISCOVERY TESTS") + print("=" * 60) + print("\nThese tests expose bugs and inconsistencies in Typer.") + print("Run with: pytest test_typer_bugs.py -v") + print("\nTo skip xfail tests: pytest test_typer_bugs.py -v --runxfail") + print("=" * 60) From 36c05292acab449a0618a34cf5fd09ec5c0af675 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci-lite[bot]" <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Date: Sun, 8 Feb 2026 12:56:28 +0000 Subject: [PATCH 3/3] =?UTF-8?q?=F0=9F=8E=A8=20Auto=20format?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tutorial001_py39.py | 1 + tests/test_typer_bugs.py | 108 +++++++++--------- 2 files changed, 56 insertions(+), 53 deletions(-) diff --git a/docs_src/multiple_values/options_with_multiple_values/tutorial001_py39.py b/docs_src/multiple_values/options_with_multiple_values/tutorial001_py39.py index 37aa212f5c..d007c76375 100644 --- a/docs_src/multiple_values/options_with_multiple_values/tutorial001_py39.py +++ b/docs_src/multiple_values/options_with_multiple_values/tutorial001_py39.py @@ -10,6 +10,7 @@ Example usage: python tutorial001_py39.py --user Harry 100 true """ + import typer app = typer.Typer() diff --git a/tests/test_typer_bugs.py b/tests/test_typer_bugs.py index e194ccf98c..fc3752ce2d 100644 --- a/tests/test_typer_bugs.py +++ b/tests/test_typer_bugs.py @@ -6,14 +6,15 @@ - Tuple vs List inconsistency: https://github.com/fastapi/typer/issues/[TBD] - Multiple value option returns tuple not list: https://github.com/fastapi/typer/issues/[TBD] -Author: Kaushaki Khandelwal +Author: Kaushaki Khandelwal Date: 2026-02-08 """ +from typing import List + import pytest -from typer.testing import CliRunner import typer -from typing import List +from typer.testing import CliRunner runner = CliRunner() @@ -21,20 +22,20 @@ class TestTupleOptionNoneDefaults: """ BUG: Tuple options with (None, None, None) defaults don't properly validate. - + The issue: When a tuple option has None values in its default tuple, the validation and type conversion doesn't handle missing values correctly. """ - + def test_tuple_option_none_default_should_handle_missing_gracefully(self): """ EXPECTED: Should allow command to run without the --user flag ACTUAL: May raise validation error or handle None incorrectly - + Related to: tutorial001_py39.py """ app = typer.Typer() - + @app.command() def main(user: tuple[str, int, bool] = typer.Option((None, None, None))): username, coins, is_wizard = user @@ -44,31 +45,31 @@ def main(user: tuple[str, int, bool] = typer.Option((None, None, None))): typer.echo(f"The username {username} has {coins} coins") if is_wizard: typer.echo("And this user is a wizard!") - + # This test currently FAILS or behaves inconsistently result = runner.invoke(app, []) - + # We expect graceful handling of missing values assert result.exit_code == 1 # Should abort gracefully assert "No user provided" in result.output - + def test_tuple_option_partial_values_should_error(self): """ BUG: Providing partial tuple values should give clear error - + EXPECTED: Clear error message about needing all 3 values ACTUAL: May accept partial values or give confusing error """ app = typer.Typer() - + @app.command() def main(user: tuple[str, int, bool] = typer.Option((None, None, None))): username, coins, is_wizard = user typer.echo(f"User: {username}") - + # Providing only 2 values when 3 are expected result = runner.invoke(app, ["--user", "Harry", "100"]) - + # Should fail with clear error assert result.exit_code != 0 assert "user" in result.output.lower() or "values" in result.output.lower() @@ -77,14 +78,14 @@ def main(user: tuple[str, int, bool] = typer.Option((None, None, None))): class TestListVsTupleInconsistency: """ BUG: Multiple value options return tuple instead of list despite type hint - + GitHub Issue: Multiple value option gives tuple and not list Reference: https://github.com/fastapi/typer/issues/[issue_number] - + The documentation states that List[str] should receive values as a list, but the actual behavior returns a tuple. """ - + @pytest.mark.xfail( reason="Known bug: multiple value option returns tuple instead of list" ) @@ -92,22 +93,22 @@ def test_list_option_should_return_list_not_tuple(self): """ EXPECTED: List[str] type hint should receive actual list ACTUAL: Receives tuple instead - + This contradicts the documentation and type hints. """ app = typer.Typer() - + received_type = None - + @app.command() def main(names: List[str] = typer.Option([], "--name", multiple=True)): nonlocal received_type received_type = type(names) typer.echo(f"Type: {type(names).__name__}") typer.echo(f"Names: {names}") - + result = runner.invoke(app, ["--name", "Alice", "--name", "Bob"]) - + assert result.exit_code == 0 # This assertion FAILS - we get tuple instead of list assert received_type == list, f"Expected list but got {received_type}" @@ -120,7 +121,7 @@ def test_list_type_hint_should_support_list_operations(self): ACTUAL: Fails because it's actually a tuple """ app = typer.Typer() - + @app.command() def main(items: List[str] = typer.Option([], multiple=True)): # Type hint says List, so we should be able to use list methods @@ -129,9 +130,9 @@ def main(items: List[str] = typer.Option([], multiple=True)): typer.echo("Success: can use list methods") except AttributeError: typer.echo("FAIL: cannot use list methods - it's a tuple!") - + result = runner.invoke(app, ["--items", "a", "--items", "b"]) - + # This test currently FAILS assert "Success" in result.output assert "FAIL" not in result.output @@ -141,30 +142,30 @@ class TestEmptyDefaultBehavior: """ BUG: Empty defaults behave inconsistently between tuple and list options """ - + def test_empty_tuple_default_behavior(self): """ Document how empty tuple defaults currently behave """ app = typer.Typer() - + @app.command() def main(values: tuple[str, str] = typer.Option(("", ""))): typer.echo(f"Values: {values}") - + # How should this behave when user provides nothing? result = runner.invoke(app, []) - + # Current behavior might be inconsistent # This test documents what happens assert result.exit_code == 0 - + def test_none_vs_empty_string_in_tuple(self): """ BUG: Inconsistent handling of None vs empty string in defaults """ app = typer.Typer() - + @app.command() def main( user1: tuple[str, int] = typer.Option((None, None)), @@ -172,9 +173,9 @@ def main( ): typer.echo(f"User1: {user1}") typer.echo(f"User2: {user2}") - + result = runner.invoke(app, []) - + # Should these behave the same? Currently they might not assert result.exit_code == 0 @@ -183,18 +184,18 @@ class TestTupleTypeConversion: """ BUG: Type conversion in tuples may not handle all cases correctly """ - + def test_tuple_bool_conversion_edge_cases(self): """ Test various bool representations in tuple options """ app = typer.Typer() - + @app.command() def main(user: tuple[str, int, bool] = typer.Option((None, None, None))): username, coins, is_wizard = user typer.echo(f"{username}: {coins} coins, wizard={is_wizard}") - + # Test various bool representations test_cases = [ (["--user", "Harry", "100", "true"], True), @@ -202,7 +203,7 @@ def main(user: tuple[str, int, bool] = typer.Option((None, None, None))): (["--user", "Hermione", "200", "1"], True), (["--user", "Draco", "150", "0"], False), ] - + for args, expected_bool in test_cases: result = runner.invoke(app, args) # Document current behavior - may have inconsistencies @@ -216,17 +217,19 @@ def main(user: tuple[str, int, bool] = typer.Option((None, None, None))): class TestRequiresDevInstall: """ These tests require Typer to be installed in development mode. - + To run: pip install -e . """ - + def test_with_actual_tutorial_file(self): """Test the actual tutorial file""" - from docs_src.multiple_values.options_with_multiple_values import tutorial001_py39 - + from docs_src.multiple_values.options_with_multiple_values import ( + tutorial001_py39, + ) + app = tutorial001_py39.app result = runner.invoke(app, []) - + assert result.exit_code == 1 assert "No user provided" in result.output @@ -236,43 +239,42 @@ class TestEdgeCases: """ Additional edge cases that might expose bugs """ - + def test_mixing_required_and_optional_tuple_options(self): """ How does Typer handle mix of required and optional tuple options? """ app = typer.Typer() - + @app.command() def main( required: tuple[str, int], # Required optional: tuple[str, int] = typer.Option((None, None)), # Optional ): typer.echo(f"Required: {required}, Optional: {optional}") - + # This might have unexpected behavior result = runner.invoke(app, ["value", "42"]) - + # Document the actual behavior print(f"Exit code: {result.exit_code}") print(f"Output: {result.output}") - + @pytest.mark.xfail(reason="Investigating behavior with nested types") def test_list_of_tuples_not_supported(self): """ BUG: List[Tuple[str, str]] should work but gives AssertionError - + Reference: GitHub discussions about List[Tuple[str, str]] support """ app = typer.Typer() - + with pytest.raises(AssertionError, match="complex sub-types"): + @app.command() - def main( - pairs: List[tuple[str, str]] = typer.Option([]) - ): + def main(pairs: List[tuple[str, str]] = typer.Option([])): typer.echo(f"Pairs: {pairs}") - + # This definition itself should raise an error # "List types with complex sub-types are not currently supported"