From 7fd10a93ff728ec193a1b275686ff4294f17b6ec Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 14 Nov 2025 11:23:08 +0000 Subject: [PATCH 01/18] Initial plan From b2d569b61c2ea86e83f77addf635f6e5cc9aae30 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 14 Nov 2025 11:34:26 +0000 Subject: [PATCH 02/18] Add 5 new Python feature modules and update all README files Co-authored-by: huangsam <515617+huangsam@users.noreply.github.com> --- README.de.md | 5 + README.es.md | 5 + README.fr.md | 5 + README.ko.md | 5 + README.md | 5 + ultimatepython/advanced/exception_groups.py | 360 +++++++++++++++++++ ultimatepython/advanced/pattern_matching.py | 319 ++++++++++++++++ ultimatepython/data_structures/dict_union.py | 206 +++++++++++ ultimatepython/syntax/arg_enforcement.py | 248 +++++++++++++ ultimatepython/syntax/walrus_operator.py | 118 ++++++ 10 files changed, 1276 insertions(+) create mode 100644 ultimatepython/advanced/exception_groups.py create mode 100644 ultimatepython/advanced/pattern_matching.py create mode 100644 ultimatepython/data_structures/dict_union.py create mode 100644 ultimatepython/syntax/arg_enforcement.py create mode 100644 ultimatepython/syntax/walrus_operator.py diff --git a/README.de.md b/README.de.md index e6838e1..baf34bd 100644 --- a/README.de.md +++ b/README.de.md @@ -88,11 +88,14 @@ Es gibt zwei Möglichkeiten, die Module auszuführen: - Conditional: [if | if-else | if-elif-else](ultimatepython/syntax/conditional.py) ( 🍰 ) - Loop: [for-loop | while-loop](ultimatepython/syntax/loop.py) ( 🍰 ) - Function: [def | lambda](ultimatepython/syntax/function.py) ( 🍰 ) + - Walrus operator: [Assignment expressions :=](ultimatepython/syntax/walrus_operator.py) ( 🤯 ) + - Argument enforcement: [Positional-only / | Keyword-only *](ultimatepython/syntax/arg_enforcement.py) ( 🤯 ) 3. **Daten-Strukturen** - List: [List operations](ultimatepython/data_structures/list.py) ( 🍰 ) - Tuple: [Tuple operations](ultimatepython/data_structures/tuple.py) - Set: [Set operations](ultimatepython/data_structures/set.py) - Dict: [Dictionary operations](ultimatepython/data_structures/dict.py) ( 🍰 ) + - Dict union: [Dictionary merge | and |=](ultimatepython/data_structures/dict_union.py) ( 🤯 ) - Comprehension: [list | tuple | set | dict](ultimatepython/data_structures/comprehension.py) - String: [String operations](ultimatepython/data_structures/string.py) ( 🍰 ) - Deque: [deque](ultimatepython/data_structures/deque.py) ( 🤯 ) @@ -121,6 +124,8 @@ Es gibt zwei Möglichkeiten, die Module auszuführen: - Regular expression: [search | findall | match | fullmatch](ultimatepython/advanced/regex.py) ( 🤯 ) - Data format: [json | xml | csv](ultimatepython/advanced/data_format.py) ( 🤯 ) - Datetime: [datetime | timezone](ultimatepython/advanced/date_time.py) ( 🤯 ) + - Pattern matching: [match | case](ultimatepython/advanced/pattern_matching.py) ( 🤯 ) + - Exception groups: [ExceptionGroup | except*](ultimatepython/advanced/exception_groups.py) ( 🤯 ) ## Zusätzliche Ressourcen diff --git a/README.es.md b/README.es.md index 173a078..54d9374 100644 --- a/README.es.md +++ b/README.es.md @@ -86,11 +86,14 @@ Hay dos maneras de ejecutar los módulos: - Condicionales: [if | if-else | if-elif-else](ultimatepython/syntax/conditional.py) ( 🍰 ) - Iteraciones: [for-loop | while-loop](ultimatepython/syntax/loop.py) ( 🍰 ) - Funciones: [def | lambda](ultimatepython/syntax/function.py) ( 🍰 ) + - Operador morsa: [Expresiones de asignación :=](ultimatepython/syntax/walrus_operator.py) ( 🤯 ) + - Aplicación de argumentos: [Solo posicional / | Solo palabra clave *](ultimatepython/syntax/arg_enforcement.py) ( 🤯 ) 3. **Estructura de datos** - Lista: [Operaciones con listas](ultimatepython/data_structures/list.py) ( 🍰 ) - Tupla: [Operaciones con tuplas](ultimatepython/data_structures/tuple.py) - Set: [Operaciones con sets](ultimatepython/data_structures/set.py) - Diccionario: [Operaciones con dicts](ultimatepython/data_structures/dict.py) ( 🍰 ) + - Unión de diccionarios: [Fusión de diccionarios | y |=](ultimatepython/data_structures/dict_union.py) ( 🤯 ) - Comprensión: [list | tuple | set | dict](ultimatepython/data_structures/comprehension.py) - Cadena: [Operaciones con strings](ultimatepython/data_structures/string.py) ( 🍰 ) - Deque: [deque](ultimatepython/data_structures/deque.py) ( 🤯 ) @@ -119,6 +122,8 @@ Hay dos maneras de ejecutar los módulos: - Expresiones regulares: [search | findall | match | fullmatch](ultimatepython/advanced/regex.py) ( 🤯 ) - Formatos de datos: [json | xml | csv](ultimatepython/advanced/data_format.py) ( 🤯 ) - Fecha y hora: [datetime | timezone](ultimatepython/advanced/date_time.py) ( 🤯 ) + - Coincidencia de patrones: [match | case](ultimatepython/advanced/pattern_matching.py) ( 🤯 ) + - Grupos de excepciones: [ExceptionGroup | except*](ultimatepython/advanced/exception_groups.py) ( 🤯 ) ## Recursos adicionales diff --git a/README.fr.md b/README.fr.md index d064e2b..d963678 100644 --- a/README.fr.md +++ b/README.fr.md @@ -95,12 +95,15 @@ Deux méthodes sont possibles :     - Conditionnelle : [if | if-else | if-elif-else](ultimatepython/syntax/conditional.py) ( 🍰 )     - Boucle : [for-loop | while-loop](ultimatepython/syntax/loop.py) ( 🍰 )     - Fonction : [def | lambda](ultimatepython/syntax/function.py) ( 🍰 ) + - Opérateur morse : [Expressions d\'affectation :=](ultimatepython/syntax/walrus_operator.py) ( 🤯 ) + - Application d\'arguments : [Positionnels uniquement / | Mots-clés uniquement *](ultimatepython/syntax/arg_enforcement.py) ( 🤯 ) 3. **Structures de données**     - Liste : [Opérations sur les listes](ultimatepython/data_structures/list.py) ( 🍰 )     - Tuple : [Opérations sur les tuples](ultimatepython/data_structures/tuple.py)     - Ensemble : [Opérations sur les ensembles](ultimatepython/data_structures/set.py)     - Dictionnaire : [Opérations sur les dictionnaires](ultimatepython/data_structures/dict.py) ( 🍰 ) + - Union de dictionnaires : [Fusion de dictionnaires | et |=](ultimatepython/data_structures/dict_union.py) ( 🤯 )     - Compréhension : [list | tuple | set | dict](ultimatepython/data_structures/comprehension.py)     - Chaîne : [Opérations sur les chaînes](ultimatepython/data_structures/string.py) ( 🍰 )     - Deque : [deque](ultimatepython/data_structures/deque.py) ( 🤯 ) @@ -131,6 +134,8 @@ Deux méthodes sont possibles :     - Expressions régulières : [search | findall | match | fullmatch](ultimatepython/advanced/regex.py) ( 🤯 )     - Format de données : [json | xml | csv](ultimatepython/advanced/data_format.py) ( 🤯 )     - Date et heure : [datetime | timezone](ultimatepython/advanced/date_time.py) ( 🤯 ) + - Correspondance de motifs : [match | case](ultimatepython/advanced/pattern_matching.py) ( 🤯 ) + - Groupes d\'exceptions : [ExceptionGroup | except*](ultimatepython/advanced/exception_groups.py) ( 🤯 ) ## Ressources supplémentaires diff --git a/README.ko.md b/README.ko.md index c2ab129..bcf4cfc 100644 --- a/README.ko.md +++ b/README.ko.md @@ -77,11 +77,14 @@ print("Ultimate Python 학습 가이드") - 조건문 : [if | if-else | if-elif-else](ultimatepython/syntax/conditional.py) ( 🍰 ) - 반복문 : [for-loop | while-loop](ultimatepython/syntax/loop.py) ( 🍰 ) - 함수 : [def | lambda](ultimatepython/syntax/function.py) ( 🍰 ) + - 바다코끼리 연산자 : [할당 표현식 :=](ultimatepython/syntax/walrus_operator.py) ( 🤯 ) + - 인수 강제 : [위치 전용 / | 키워드 전용 *](ultimatepython/syntax/arg_enforcement.py) ( 🤯 ) 3. **데이터 구조** - 리스트 : [리스트 연산](ultimatepython/data_structures/list.py) ( 🍰 ) - 튜플 : [튜플 연산](ultimatepython/data_structures/tuple.py) - 세트 : [세트 연산](ultimatepython/data_structures/set.py) - 딕셔너리 : [딕셔너리 연산](ultimatepython/data_structures/dict.py) ( 🍰 ) + - 딕셔너리 합병 : [딕셔너리 병합 | 및 |=](ultimatepython/data_structures/dict_union.py) ( 🤯 ) - 컴프리헨션 : [리스트 | 튜플 | 세트 | 딕셔너리](ultimatepython/data_structures/comprehension.py) - 문자열 : [문자열 연산](ultimatepython/data_structures/string.py) ( 🍰 ) - 덱: [deque](ultimatepython/data_structures/deque.py) ( 🤯 ) @@ -110,6 +113,8 @@ print("Ultimate Python 학습 가이드") - 정규식 : [search | findall | match | fullmatch](ultimatepython/advanced/regex.py) ( 🤯 ) - 데이터 포맷 : [json | xml | csv](ultimatepython/advanced/data_format.py) ( 🤯 ) - 날짜와 시간 : [datetime | timezone](ultimatepython/advanced/date_time.py) ( 🤯 ) + - 패턴 매칭 : [match | case](ultimatepython/advanced/pattern_matching.py) ( 🤯 ) + - 예외 그룹 : [ExceptionGroup | except*](ultimatepython/advanced/exception_groups.py) ( 🤯 ) ## 추가 자료 diff --git a/README.md b/README.md index e30df42..c027d22 100644 --- a/README.md +++ b/README.md @@ -89,11 +89,14 @@ There are two ways of running the modules: - Conditional: [if | if-else | if-elif-else](ultimatepython/syntax/conditional.py) ( 🍰 ) - Loop: [for-loop | while-loop](ultimatepython/syntax/loop.py) ( 🍰 ) - Function: [def | lambda](ultimatepython/syntax/function.py) ( 🍰 ) + - Walrus operator: [Assignment expressions :=](ultimatepython/syntax/walrus_operator.py) ( 🤯 ) + - Argument enforcement: [Positional-only / | Keyword-only *](ultimatepython/syntax/arg_enforcement.py) ( 🤯 ) 3. **Data Structures** - List: [List operations](ultimatepython/data_structures/list.py) ( 🍰 ) - Tuple: [Tuple operations](ultimatepython/data_structures/tuple.py) - Set: [Set operations](ultimatepython/data_structures/set.py) - Dict: [Dictionary operations](ultimatepython/data_structures/dict.py) ( 🍰 ) + - Dict union: [Dictionary merge | and |=](ultimatepython/data_structures/dict_union.py) ( 🤯 ) - Comprehension: [list | tuple | set | dict](ultimatepython/data_structures/comprehension.py) - String: [String operations](ultimatepython/data_structures/string.py) ( 🍰 ) - Deque: [deque](ultimatepython/data_structures/deque.py) ( 🤯 ) @@ -122,6 +125,8 @@ There are two ways of running the modules: - Regular expression: [search | findall | match | fullmatch](ultimatepython/advanced/regex.py) ( 🤯 ) - Data format: [json | xml | csv](ultimatepython/advanced/data_format.py) ( 🤯 ) - Datetime: [datetime | timezone](ultimatepython/advanced/date_time.py) ( 🤯 ) + - Pattern matching: [match | case](ultimatepython/advanced/pattern_matching.py) ( 🤯 ) + - Exception groups: [ExceptionGroup | except*](ultimatepython/advanced/exception_groups.py) ( 🤯 ) ## Additional resources diff --git a/ultimatepython/advanced/exception_groups.py b/ultimatepython/advanced/exception_groups.py new file mode 100644 index 0000000..b47c668 --- /dev/null +++ b/ultimatepython/advanced/exception_groups.py @@ -0,0 +1,360 @@ +""" +Exception Groups allow you to raise and handle multiple exceptions +simultaneously. This is particularly useful for concurrent code where +multiple operations can fail independently. + +Exception Groups were introduced in Python 3.11 through PEP 654. They use +the ExceptionGroup class and the new except* syntax for handling multiple +exception types from a group. +""" + + +def raising_exception_groups(): + """Demonstrate how to raise exception groups. + + ExceptionGroup is a built-in exception type that contains multiple + exceptions. You create it with a message and a sequence of exceptions. + """ + # Create a simple exception group with multiple exceptions + exceptions = [ + ValueError("Invalid value"), + TypeError("Wrong type"), + KeyError("Missing key") + ] + + try: + # Raise an ExceptionGroup containing multiple exceptions + raise ExceptionGroup("Multiple errors occurred", exceptions) + except ExceptionGroup as eg: + # We can access the exceptions in the group + assert len(eg.exceptions) == 3 + assert isinstance(eg.exceptions[0], ValueError) + assert isinstance(eg.exceptions[1], TypeError) + assert isinstance(eg.exceptions[2], KeyError) + return "Caught exception group" + + return "Should not reach here" + + +def handling_with_except_star(): + """Demonstrate the except* syntax for handling exception groups. + + The except* syntax allows you to handle specific exception types + from an ExceptionGroup while leaving others unhandled. + """ + results = [] + + try: + exceptions = [ + ValueError("Invalid value 1"), + ValueError("Invalid value 2"), + TypeError("Wrong type 1") + ] + raise ExceptionGroup("Processing errors", exceptions) + except* ValueError as eg: + # This catches all ValueError instances from the group + # Note: eg is still an ExceptionGroup, but only containing ValueErrors + assert len(eg.exceptions) == 2 + results.append("Caught ValueErrors") + except* TypeError as eg: + # This catches all TypeError instances from the group + assert len(eg.exceptions) == 1 + results.append("Caught TypeError") + + return results + + +def handling_multiple_types(): + """Handle different exception types from a group separately. + + You can have multiple except* blocks to handle different + exception types independently. + """ + results = [] + + try: + exceptions = [ + ValueError("Bad value"), + TypeError("Bad type"), + RuntimeError("Runtime issue"), + ValueError("Another bad value") + ] + raise ExceptionGroup("Various errors", exceptions) + except* ValueError as eg: + # Handles all ValueErrors (there are 2) + results.append(f"ValueError count: {len(eg.exceptions)}") + except* TypeError as eg: + # Handles all TypeErrors (there is 1) + results.append(f"TypeError count: {len(eg.exceptions)}") + except* RuntimeError as eg: + # Handles all RuntimeErrors (there is 1) + results.append(f"RuntimeError count: {len(eg.exceptions)}") + + return results + + +def nested_exception_groups(): + """Exception groups can be nested. + + You can have an ExceptionGroup that contains other ExceptionGroups. + Note: nested ExceptionGroups are treated as single exceptions themselves + unless you explicitly unwrap them. + """ + # Create a flat exception group with mixed exception types + exceptions = [ + ValueError("Value error 1"), + ValueError("Value error 2"), + TypeError("Type error 1"), + RuntimeError("Runtime error") + ] + + eg = ExceptionGroup("Multiple errors", exceptions) + + results = [] + + try: + raise eg + except* ValueError as eg: + # Catches all ValueErrors from the group + results.append(f"Caught {len(eg.exceptions)} ValueErrors") + except* TypeError as eg: + results.append(f"Caught {len(eg.exceptions)} TypeErrors") + except* RuntimeError as eg: + results.append(f"Caught {len(eg.exceptions)} RuntimeErrors") + + return results + + +def partial_handling(): + """Demonstrate partial handling of exception groups. + + If you don't handle all exceptions in a group, the unhandled ones + are re-raised as a new ExceptionGroup. + """ + results = [] + + try: + try: + exceptions = [ + ValueError("Value error"), + TypeError("Type error"), + KeyError("Key error") + ] + raise ExceptionGroup("Mixed errors", exceptions) + except* ValueError as eg: + # Only handle ValueError, leaving TypeError and KeyError + results.append("Handled ValueError") + # The other exceptions are automatically re-raised + + except ExceptionGroup as eg: + # The re-raised group contains only the unhandled exceptions + assert len(eg.exceptions) == 2 + assert isinstance(eg.exceptions[0], TypeError) + assert isinstance(eg.exceptions[1], KeyError) + results.append("Caught remaining exceptions") + + return results + + +def filtering_exceptions(): + """Filter and process exceptions from a group. + + The subgroup() method allows you to filter exceptions by type. + """ + exceptions = [ + ValueError("Value 1"), + TypeError("Type 1"), + ValueError("Value 2"), + RuntimeError("Runtime 1") + ] + + eg = ExceptionGroup("All errors", exceptions) + + # Get a subgroup containing only ValueErrors + value_errors = eg.subgroup(ValueError) + assert value_errors is not None + assert len(value_errors.exceptions) == 2 + + # Get a subgroup containing only TypeErrors + type_errors = eg.subgroup(TypeError) + assert type_errors is not None + assert len(type_errors.exceptions) == 1 + + # If no matching exceptions, subgroup returns None + key_errors = eg.subgroup(KeyError) + assert key_errors is None + + return "Filtering successful" + + +def practical_concurrent_example(): + """A practical example simulating concurrent operations. + + This demonstrates a common use case: running multiple tasks + where each can fail independently. + """ + def process_user(user_id): + """Simulate processing a user.""" + if user_id == 1: + raise ValueError(f"Invalid user ID: {user_id}") + elif user_id == 3: + raise ConnectionError(f"Cannot connect for user: {user_id}") + return f"Processed user {user_id}" + + user_ids = [1, 2, 3, 4] + errors = [] + successes = [] + + # Simulate processing multiple users + for user_id in user_ids: + try: + result = process_user(user_id) + successes.append(result) + except Exception as e: + errors.append(e) + + # If there were errors, raise them all together + results = {"successes": len(successes), "errors": 0} + + if errors: + try: + raise ExceptionGroup("User processing errors", errors) + except* ValueError as eg: + results["errors"] += len(eg.exceptions) + except* ConnectionError as eg: + results["errors"] += len(eg.exceptions) + + return results + + +def exception_group_with_notes(): + """Exception groups work with exception notes (also new in Python 3.11). + + You can add contextual information to exceptions using add_note(). + """ + results = [] + + try: + exc1 = ValueError("Invalid input") + exc1.add_note("Occurred in data validation") + exc1.add_note("User input was: 'abc123'") + + exc2 = TypeError("Wrong type provided") + exc2.add_note("Expected int, got str") + + raise ExceptionGroup("Processing failed", [exc1, exc2]) + + except* ValueError as eg: + # The notes are preserved in the exception + exception = eg.exceptions[0] + assert hasattr(exception, "__notes__") + results.append("ValueError with notes caught") + + except* TypeError as eg: + exception = eg.exceptions[0] + assert hasattr(exception, "__notes__") + results.append("TypeError with notes caught") + + return results + + +def main() -> None: + # Test basic exception group raising and catching + result1 = raising_exception_groups() + assert result1 == "Caught exception group" + + # Test except* syntax + result2 = handling_with_except_star() + assert result2 == ["Caught ValueErrors", "Caught TypeError"] + + # Test handling multiple exception types + result3 = handling_multiple_types() + assert "ValueError count: 2" in result3 + assert "TypeError count: 1" in result3 + assert "RuntimeError count: 1" in result3 + + # Test nested exception groups + result4 = nested_exception_groups() + assert len(result4) == 3 + assert "Caught 2 ValueErrors" in result4 + assert "Caught 1 TypeErrors" in result4 + assert "Caught 1 RuntimeErrors" in result4 + + # Test partial handling + result5 = partial_handling() + assert result5 == ["Handled ValueError", "Caught remaining exceptions"] + + # Test filtering exceptions + result6 = filtering_exceptions() + assert result6 == "Filtering successful" + + # Test practical concurrent example + result7 = practical_concurrent_example() + assert result7["successes"] == 2 # Users 2 and 4 processed successfully + assert result7["errors"] == 2 # Users 1 and 3 had errors + + # Test exception groups with notes + result8 = exception_group_with_notes() + assert len(result8) == 2 + + # You can also split exception groups programmatically + def split_by_type(eg): + """Split an exception group into separate groups by type.""" + value_errors = eg.subgroup(ValueError) + type_errors = eg.subgroup(TypeError) + return value_errors, type_errors + + exceptions = [ + ValueError("Value 1"), + TypeError("Type 1"), + ValueError("Value 2") + ] + eg = ExceptionGroup("Mixed", exceptions) + values, types = split_by_type(eg) + + assert values is not None and len(values.exceptions) == 2 + assert types is not None and len(types.exceptions) == 1 + + # The derive() method creates a new ExceptionGroup with the same message + new_eg = eg.derive([RuntimeError("New error")]) + assert new_eg.message == eg.message + assert len(new_eg.exceptions) == 1 + assert isinstance(new_eg.exceptions[0], RuntimeError) + + # Exception groups are particularly valuable in async code + # where multiple coroutines can fail simultaneously + def simulate_async_failures(): + """Simulate collecting errors from multiple async operations.""" + # In real async code, you might use asyncio.gather with return_exceptions=True + task_errors = [ + TimeoutError("Task 1 timed out"), + ValueError("Task 2 invalid input"), + ConnectionError("Task 3 connection failed") + ] + + results = {"timeout": 0, "value": 0, "connection": 0} + + try: + raise ExceptionGroup("Async task failures", task_errors) + except* TimeoutError: + results["timeout"] += 1 + except* ValueError: + results["value"] += 1 + except* ConnectionError: + results["connection"] += 1 + + return results + + async_results = simulate_async_failures() + assert async_results["timeout"] == 1 + assert async_results["value"] == 1 + assert async_results["connection"] == 1 + + # Traditional exception handling vs exception groups: + # Traditional: You can only catch one exception at a time + # Exception groups: You can catch and handle multiple exceptions simultaneously + # This is crucial for modern concurrent and parallel programming patterns + + +if __name__ == "__main__": + main() diff --git a/ultimatepython/advanced/pattern_matching.py b/ultimatepython/advanced/pattern_matching.py new file mode 100644 index 0000000..0b66482 --- /dev/null +++ b/ultimatepython/advanced/pattern_matching.py @@ -0,0 +1,319 @@ +""" +Structural pattern matching allows you to match complex data structures +against patterns and extract values in a clean, readable way. This feature +is similar to switch-case statements in other languages but much more powerful. + +Pattern matching was introduced in Python 3.10 through PEP 634, PEP 635, +and PEP 636. It uses the 'match' and 'case' keywords. +""" + + +def classify_number(value): + """Classify a number using pattern matching with literals. + + This demonstrates matching against specific literal values. + """ + match value: + case 0: + return "zero" + case 1: + return "one" + case 2: + return "two" + case _: + # The underscore _ is a wildcard that matches anything + return "other" + + +def classify_http_status(status): + """Classify HTTP status codes using pattern matching. + + This shows how pattern matching can make code more readable + than a series of if-elif-else statements. + """ + match status: + case 200: + return "OK" + case 201: + return "Created" + case 400: + return "Bad Request" + case 401: + return "Unauthorized" + case 404: + return "Not Found" + case 500: + return "Internal Server Error" + case _: + return "Unknown Status" + + +def process_point(point): + """Process a point tuple using pattern matching with sequences. + + This demonstrates pattern matching against tuple structures + and extracting values from them. + """ + match point: + case (0, 0): + return "Origin" + case (0, y): + # Match any point on the y-axis, capture y coordinate + return f"Y-axis at y={y}" + case (x, 0): + # Match any point on the x-axis, capture x coordinate + return f"X-axis at x={x}" + case (x, y): + # Match any other point, capture both coordinates + return f"Point at ({x}, {y})" + case _: + return "Not a valid 2D point" + + +def analyze_sequence(data): + """Analyze sequences using pattern matching. + + This shows how to match lists with specific structures + and extract elements. + """ + match data: + case []: + return "Empty list" + case [x]: + # Match a list with exactly one element + return f"Single element: {x}" + case [x, y]: + # Match a list with exactly two elements + return f"Pair: {x}, {y}" + case [first, *rest]: + # Match a list with at least one element + # The *rest captures remaining elements + return f"First: {first}, Rest: {rest}" + case _: + return "Not a list" + + +def process_command(command): + """Process commands using pattern matching with guards. + + Guards are if conditions that provide additional filtering + after a pattern match. + """ + match command: + case ["quit"]: + return "Quitting" + case ["go", direction] if direction in ["north", "south", "east", "west"]: + # Guard clause: only match if direction is valid + return f"Going {direction}" + case ["go", direction]: + # This matches any direction that failed the guard above + return f"Invalid direction: {direction}" + case ["take", item]: + return f"Taking {item}" + case ["take", item, quantity] if quantity > 0: + return f"Taking {quantity} {item}" + case ["take", item, quantity]: + return f"Invalid quantity: {quantity}" + case _: + return "Unknown command" + + +class Point: + """A simple 2D point class for pattern matching examples.""" + + def __init__(self, x, y): + self.x = x + self.y = y + + +class Circle: + """A circle with center and radius for pattern matching examples.""" + + def __init__(self, center, radius): + self.center = center + self.radius = radius + + +def describe_shape(shape): + """Describe shapes using pattern matching with class patterns. + + This demonstrates matching against class instances and + extracting their attributes. + """ + match shape: + case Point(x=0, y=0): + # Match a Point at the origin + return "Point at origin" + case Point(x=0, y=y): + # Match a Point on the y-axis + return f"Point on Y-axis at {y}" + case Point(x=x, y=0): + # Match a Point on the x-axis + return f"Point on X-axis at {x}" + case Point(x=x, y=y) if x == y: + # Match a Point on the diagonal line y=x + return f"Point on diagonal at ({x}, {y})" + case Point(x=x, y=y): + # Match any other Point + return f"Point at ({x}, {y})" + case Circle(center=Point(x=0, y=0), radius=r): + # Match a Circle centered at origin + return f"Circle at origin with radius {r}" + case Circle(center=Point(x=x, y=y), radius=r): + # Match any other Circle + return f"Circle at ({x}, {y}) with radius {r}" + case _: + return "Unknown shape" + + +def process_json_data(data): + """Process JSON-like dictionary data with pattern matching. + + This shows how to match against dictionary structures. + """ + match data: + case {"type": "user", "name": name, "age": age}: + return f"User {name} is {age} years old" + case {"type": "user", "name": name}: + # Match user without age + return f"User {name} with unknown age" + case {"type": "product", "name": name, "price": price} if price > 0: + return f"Product {name} costs ${price}" + case {"type": "product", "name": name, "price": price}: + return f"Product {name} has invalid price: {price}" + case {"type": type_name}: + # Match any dict with a type key + return f"Unknown type: {type_name}" + case _: + return "Invalid data" + + +def main() -> None: + # Test literal pattern matching + assert classify_number(0) == "zero" + assert classify_number(1) == "one" + assert classify_number(2) == "two" + assert classify_number(5) == "other" + assert classify_number(100) == "other" + + # Test HTTP status classification + assert classify_http_status(200) == "OK" + assert classify_http_status(404) == "Not Found" + assert classify_http_status(999) == "Unknown Status" + + # Test sequence pattern matching with tuples + assert process_point((0, 0)) == "Origin" + assert process_point((0, 5)) == "Y-axis at y=5" + assert process_point((3, 0)) == "X-axis at x=3" + assert process_point((4, 7)) == "Point at (4, 7)" + + # Test sequence pattern matching with lists + assert analyze_sequence([]) == "Empty list" + assert analyze_sequence([42]) == "Single element: 42" + assert analyze_sequence([1, 2]) == "Pair: 1, 2" + assert analyze_sequence([1, 2, 3, 4]) == "First: 1, Rest: [2, 3, 4]" + assert analyze_sequence("not a list") == "Not a list" + + # Test pattern matching with guards + assert process_command(["quit"]) == "Quitting" + assert process_command(["go", "north"]) == "Going north" + assert process_command(["go", "west"]) == "Going west" + assert process_command(["go", "up"]) == "Invalid direction: up" + assert process_command(["take", "key"]) == "Taking key" + assert process_command(["take", "coin", 5]) == "Taking 5 coin" + assert process_command(["take", "coin", 0]) == "Invalid quantity: 0" + assert process_command(["take", "coin", -1]) == "Invalid quantity: -1" + assert process_command(["jump"]) == "Unknown command" + + # Test class pattern matching + p1 = Point(0, 0) + assert describe_shape(p1) == "Point at origin" + + p2 = Point(0, 5) + assert describe_shape(p2) == "Point on Y-axis at 5" + + p3 = Point(3, 0) + assert describe_shape(p3) == "Point on X-axis at 3" + + p4 = Point(5, 5) + assert describe_shape(p4) == "Point on diagonal at (5, 5)" + + p5 = Point(3, 7) + assert describe_shape(p5) == "Point at (3, 7)" + + c1 = Circle(Point(0, 0), 10) + assert describe_shape(c1) == "Circle at origin with radius 10" + + c2 = Circle(Point(5, 5), 3) + assert describe_shape(c2) == "Circle at (5, 5) with radius 3" + + # Test dictionary pattern matching + user1 = {"type": "user", "name": "Alice", "age": 30} + assert process_json_data(user1) == "User Alice is 30 years old" + + user2 = {"type": "user", "name": "Bob"} + assert process_json_data(user2) == "User Bob with unknown age" + + product1 = {"type": "product", "name": "Laptop", "price": 999} + assert process_json_data(product1) == "Product Laptop costs $999" + + product2 = {"type": "product", "name": "Free Sample", "price": 0} + assert process_json_data(product2) == "Product Free Sample has invalid price: 0" + + unknown = {"type": "order"} + assert process_json_data(unknown) == "Unknown type: order" + + invalid = {"data": "something"} + assert process_json_data(invalid) == "Invalid data" + + # Pattern matching with OR patterns + def check_value(val): + match val: + case 0 | 1 | 2: + # Match any of these values + return "small" + case 3 | 4 | 5: + return "medium" + case _: + return "large" + + assert check_value(0) == "small" + assert check_value(2) == "small" + assert check_value(3) == "medium" + assert check_value(10) == "large" + + # Pattern matching with AS patterns (walrus-like capture) + def process_range(data): + match data: + case [x, y] as pair if x < y: + # Capture the entire matched value with 'as' + return f"Valid range: {pair}" + case [x, y]: + return f"Invalid range: [{x}, {y}]" + case _: + return "Not a pair" + + assert process_range([1, 5]) == "Valid range: [1, 5]" + assert process_range([5, 1]) == "Invalid range: [5, 1]" + + # Nested pattern matching + def analyze_nested(data): + match data: + case [["pair", x, y], ["pair", a, b]]: + # Match nested structure + return f"Two pairs: ({x},{y}) and ({a},{b})" + case [["single", val]]: + return f"Single value: {val}" + case _: + return "Unknown structure" + + assert analyze_nested([["pair", 1, 2], ["pair", 3, 4]]) == "Two pairs: (1,2) and (3,4)" + assert analyze_nested([["single", 42]]) == "Single value: 42" + + # Pattern matching is particularly useful for parsing and handling + # structured data like API responses, configuration files, or + # abstract syntax trees in compilers/interpreters + + +if __name__ == "__main__": + main() diff --git a/ultimatepython/data_structures/dict_union.py b/ultimatepython/data_structures/dict_union.py new file mode 100644 index 0000000..e6a01d7 --- /dev/null +++ b/ultimatepython/data_structures/dict_union.py @@ -0,0 +1,206 @@ +""" +Dictionary union operators allow you to merge dictionaries using +the | (union) and |= (in-place union) operators. These operators +provide a clean and intuitive syntax for combining dictionaries. + +This feature was introduced in Python 3.9 through PEP 584. Before +this, you had to use methods like dict.update() or {**dict1, **dict2} +syntax to merge dictionaries. +""" + + +def main() -> None: + # Traditional dictionary merging before Python 3.9 + # Method 1: Using dict.update() (modifies the original) + dict1_old = {"a": 1, "b": 2} + dict2_old = {"c": 3, "d": 4} + dict1_old.update(dict2_old) + assert dict1_old == {"a": 1, "b": 2, "c": 3, "d": 4} + + # Method 2: Using dictionary unpacking (creates a new dict) + dict3_old = {"a": 1, "b": 2} + dict4_old = {"c": 3, "d": 4} + merged_old = {**dict3_old, **dict4_old} + assert merged_old == {"a": 1, "b": 2, "c": 3, "d": 4} + + # With Python 3.9+, we can use the | operator for union + # This creates a new dictionary without modifying the originals + dict1 = {"a": 1, "b": 2} + dict2 = {"c": 3, "d": 4} + merged = dict1 | dict2 + assert merged == {"a": 1, "b": 2, "c": 3, "d": 4} + + # The original dictionaries remain unchanged + assert dict1 == {"a": 1, "b": 2} + assert dict2 == {"c": 3, "d": 4} + + # When there are overlapping keys, the right operand's values take precedence + # This is the same behavior as dict.update() and {**d1, **d2} + dict3 = {"a": 1, "b": 2, "c": 3} + dict4 = {"b": 20, "c": 30, "d": 4} + merged2 = dict3 | dict4 + # Keys 'b' and 'c' from dict4 override those from dict3 + assert merged2 == {"a": 1, "b": 20, "c": 30, "d": 4} + + # The order matters! Left operand is the base, right operand overwrites + merged3 = dict4 | dict3 + # Now keys 'b' and 'c' from dict3 override those from dict4 + assert merged3 == {"b": 2, "c": 3, "d": 4, "a": 1} + + # The |= operator performs in-place union (augmented assignment) + # This is equivalent to dict.update() but with cleaner syntax + dict5 = {"a": 1, "b": 2} + dict6 = {"c": 3, "d": 4} + dict5 |= dict6 + # dict5 is modified in place + assert dict5 == {"a": 1, "b": 2, "c": 3, "d": 4} + # dict6 remains unchanged + assert dict6 == {"c": 3, "d": 4} + + # The |= operator also handles overlapping keys + dict7 = {"a": 1, "b": 2, "c": 3} + dict8 = {"b": 20, "d": 4} + dict7 |= dict8 + # 'b' from dict8 overwrites 'b' in dict7 + assert dict7 == {"a": 1, "b": 20, "c": 3, "d": 4} + + # You can chain multiple | operations + dict9 = {"a": 1} + dict10 = {"b": 2} + dict11 = {"c": 3} + dict12 = {"d": 4} + combined = dict9 | dict10 | dict11 | dict12 + assert combined == {"a": 1, "b": 2, "c": 3, "d": 4} + + # When chaining with overlapping keys, rightmost values win + dict13 = {"a": 1, "x": 10} + dict14 = {"b": 2, "x": 20} + dict15 = {"c": 3, "x": 30} + combined2 = dict13 | dict14 | dict15 + # 'x' ends up with value 30 from the rightmost dictionary + assert combined2 == {"a": 1, "b": 2, "c": 3, "x": 30} + + # The union operator works with empty dictionaries + empty = {} + dict16 = {"a": 1, "b": 2} + assert empty | dict16 == {"a": 1, "b": 2} + assert dict16 | empty == {"a": 1, "b": 2} + assert empty | empty == {} + + # The union operator can be used with dict() constructor results + dict17 = dict(a=1, b=2) + dict18 = dict(c=3, d=4) + merged4 = dict17 | dict18 + assert merged4 == {"a": 1, "b": 2, "c": 3, "d": 4} + + # You can mix different value types in merged dictionaries + dict19 = {"name": "Alice", "age": 30} + dict20 = {"city": "NYC", "scores": [85, 90, 95]} + dict21 = {"active": True} + person = dict19 | dict20 | dict21 + assert person == { + "name": "Alice", + "age": 30, + "city": "NYC", + "scores": [85, 90, 95], + "active": True + } + + # Practical use case: Configuration merging + # Start with default configuration + default_config = { + "timeout": 30, + "retries": 3, + "debug": False, + "log_level": "INFO" + } + + # User provides custom configuration (partial) + user_config = { + "timeout": 60, + "debug": True + } + + # Merge configurations, user settings override defaults + final_config = default_config | user_config + assert final_config == { + "timeout": 60, # Overridden by user + "retries": 3, # From default + "debug": True, # Overridden by user + "log_level": "INFO" # From default + } + + # Practical use case: Building objects incrementally + # Start with base attributes + base = {"id": 1, "type": "user"} + + # Add authentication info + with_auth = base | {"username": "john", "email": "john@example.com"} + + # Add profile info + with_profile = with_auth | {"bio": "Developer", "location": "USA"} + + assert with_profile == { + "id": 1, + "type": "user", + "username": "john", + "email": "john@example.com", + "bio": "Developer", + "location": "USA" + } + + # Practical use case: Updating records with |= + user_record = { + "id": 100, + "name": "Jane", + "status": "active", + "login_count": 5 + } + + # Apply update from an external source + update = {"status": "inactive", "login_count": 6, "last_login": "2024-01-15"} + user_record |= update + + assert user_record == { + "id": 100, + "name": "Jane", + "status": "inactive", + "login_count": 6, + "last_login": "2024-01-15" + } + + # The union operators only work with dictionaries + # Attempting to use them with non-dict types raises TypeError + dict22 = {"a": 1} + error_raised = False + try: + # This will fail because list is not a dict + invalid = dict22 | [("b", 2)] + except TypeError: + error_raised = True + assert error_raised is True + + # However, you can use dict() to convert compatible types first + dict23 = {"a": 1} + dict24 = dict([("b", 2), ("c", 3)]) # Convert list of tuples to dict + merged5 = dict23 | dict24 + assert merged5 == {"a": 1, "b": 2, "c": 3} + + # Comparison with the older approaches shows the clarity improvement: + + # OLD: Using update() - modifies original, no expression result + old1 = {"a": 1} + old1.update({"b": 2}) + assert old1 == {"a": 1, "b": 2} + + # OLD: Using unpacking - verbose for multiple merges + old2 = {**{"a": 1}, **{"b": 2}, **{"c": 3}} + assert old2 == {"a": 1, "b": 2, "c": 3} + + # NEW: Using union operator - clean and chainable + new1 = {"a": 1} | {"b": 2} | {"c": 3} + assert new1 == {"a": 1, "b": 2, "c": 3} + + +if __name__ == "__main__": + main() diff --git a/ultimatepython/syntax/arg_enforcement.py b/ultimatepython/syntax/arg_enforcement.py new file mode 100644 index 0000000..6546cae --- /dev/null +++ b/ultimatepython/syntax/arg_enforcement.py @@ -0,0 +1,248 @@ +""" +Positional-only and keyword-only parameters allow you to enforce how +arguments are passed to a function. This feature helps prevent misuse +of function APIs and makes code more maintainable. + +- Positional-only parameters (/) were introduced in Python 3.8 (PEP 570) +- Keyword-only parameters (*) were introduced in Python 3.0 (PEP 3102) + +These features give you fine-grained control over your function signatures. +""" + + +def traditional_function(a, b, c): + """A traditional function where all parameters can be passed either way. + + This function accepts arguments positionally or by keyword name. + While flexible, this can lead to API instability if parameter names change. + """ + return a + b + c + + +def positional_only(a, b, /): + """Function with positional-only parameters. + + The / symbol marks that parameters before it MUST be passed positionally. + This is useful when parameter names are not meaningful or when you want + to reserve the right to change parameter names without breaking callers. + + Parameters before / cannot be passed as keyword arguments. + """ + return a + b + + +def keyword_only(*, x, y): + """Function with keyword-only parameters. + + The * symbol marks that parameters after it MUST be passed by keyword. + This is useful for improving readability at call sites and preventing + accidental argument order mistakes. + + Parameters after * cannot be passed positionally. + """ + return x * y + + +def mixed_parameters(pos_only, /, pos_or_kw, *, kw_only): + """Function that combines all parameter types. + + - pos_only: Must be passed positionally (before /) + - pos_or_kw: Can be passed either way (between / and *) + - kw_only: Must be passed by keyword (after *) + + This gives maximum control over the function interface. + """ + return f"{pos_only}-{pos_or_kw}-{kw_only}" + + +def positional_with_defaults(a, b=10, /): + """Positional-only parameters can have default values. + + Default values work the same way as in traditional functions, + but the parameters still must be passed positionally if provided. + """ + return a + b + + +def keyword_with_defaults(*, x=5, y=3): + """Keyword-only parameters can have default values. + + When providing arguments, you must use the keyword names. + """ + return x ** y + + +def complex_signature(a, b, /, c, d=10, *, e, f=20): + """A function demonstrating a complex but valid signature. + + - a, b: positional-only + - c: positional-or-keyword (no default) + - d: positional-or-keyword (with default) + - e: keyword-only (no default) + - f: keyword-only (with default) + """ + return a + b + c + d + e + f + + +def main() -> None: + # Traditional function: can be called either way + assert traditional_function(1, 2, 3) == 6 + assert traditional_function(a=1, b=2, c=3) == 6 + assert traditional_function(1, b=2, c=3) == 6 + + # Positional-only function: must use positional arguments + assert positional_only(5, 3) == 8 + + # Trying to use keyword arguments with positional-only parameters + # will raise a TypeError + positional_error = False + try: + # This will fail because 'a' and 'b' are positional-only + positional_only(a=5, b=3) + except TypeError: + positional_error = True + assert positional_error is True + + # You also can't mix positional and keyword for positional-only params + positional_error2 = False + try: + # This will fail because 'b' is positional-only + positional_only(5, b=3) + except TypeError: + positional_error2 = True + assert positional_error2 is True + + # Keyword-only function: must use keyword arguments + assert keyword_only(x=4, y=5) == 20 + + # Trying to use positional arguments with keyword-only parameters + # will raise a TypeError + keyword_error = False + try: + # This will fail because 'x' and 'y' are keyword-only + keyword_only(4, 5) + except TypeError: + keyword_error = True + assert keyword_error is True + + # Mixed parameters demonstrate all three types + result = mixed_parameters("first", "second", kw_only="third") + assert result == "first-second-third" + + # The middle parameter can be passed either way + result2 = mixed_parameters("first", pos_or_kw="second", kw_only="third") + assert result2 == "first-second-third" + + # But positional-only must be positional + mixed_error = False + try: + mixed_parameters(pos_only="first", pos_or_kw="second", kw_only="third") + except TypeError: + mixed_error = True + assert mixed_error is True + + # And keyword-only must be keyword + mixed_error2 = False + try: + mixed_parameters("first", "second", "third") + except TypeError: + mixed_error2 = True + assert mixed_error2 is True + + # Positional-only with defaults + assert positional_with_defaults(5) == 15 # Uses default b=10 + assert positional_with_defaults(5, 20) == 25 # Overrides b with 20 + + # Even with defaults, must use positional syntax + positional_default_error = False + try: + positional_with_defaults(a=5, b=20) + except TypeError: + positional_default_error = True + assert positional_default_error is True + + # Keyword-only with defaults + assert keyword_with_defaults() == 125 # Uses defaults: 5^3 + assert keyword_with_defaults(x=2) == 8 # 2^3 + assert keyword_with_defaults(y=2) == 25 # 5^2 + assert keyword_with_defaults(x=3, y=4) == 81 # 3^4 + + # Must still use keyword syntax even when providing defaults + keyword_default_error = False + try: + keyword_with_defaults(2, 3) + except TypeError: + keyword_default_error = True + assert keyword_default_error is True + + # Complex signature: demonstrating all parameter types + # Minimal call with required params only + result3 = complex_signature(1, 2, 3, e=4) + assert result3 == 40 # 1+2+3+10(default)+4+20(default) + + # Providing all parameters + result4 = complex_signature(1, 2, 3, 4, e=5, f=6) + assert result4 == 21 # 1+2+3+4+5+6 + + # Middle parameter 'c' can be passed by keyword + result5 = complex_signature(1, 2, c=3, e=4) + assert result5 == 40 + + # Parameter 'd' can also be passed by keyword + result6 = complex_signature(1, 2, 3, d=15, e=4) + assert result6 == 45 # 1+2+3+15+4+20 + + # But 'a' and 'b' must be positional + complex_error = False + try: + complex_signature(a=1, b=2, c=3, e=4) + except TypeError: + complex_error = True + assert complex_error is True + + # And 'e' must be keyword (even though 'f' has a default) + complex_error2 = False + try: + complex_signature(1, 2, 3, 10, 4) + except TypeError: + complex_error2 = True + assert complex_error2 is True + + # Practical use case: Positional-only is great for functions where + # parameter names are not meaningful or may change + def distance(x1, y1, x2, y2, /): + """Calculate distance between two points. + + The parameter names here are somewhat arbitrary (could be p1_x, etc.) + so we make them positional-only to give us flexibility to rename them + without breaking existing code. + """ + return ((x2 - x1) ** 2 + (y2 - y1) ** 2) ** 0.5 + + assert abs(distance(0, 0, 3, 4) - 5.0) < 0.01 + + # Practical use case: Keyword-only is great for boolean flags or + # optional parameters where the intent should be clear at call site + def create_user(username, *, admin=False, active=True, send_email=False): + """Create a user with clear intent for optional parameters. + + Making admin, active, and send_email keyword-only ensures that + callers must specify exactly what they're setting, improving + readability and preventing accidental mistakes. + """ + return { + "username": username, + "admin": admin, + "active": active, + "send_email": send_email + } + + # Clear intent at call site + user = create_user("john_doe", admin=True, send_email=True) + assert user["admin"] is True + assert user["active"] is True # Used default + assert user["send_email"] is True + + +if __name__ == "__main__": + main() diff --git a/ultimatepython/syntax/walrus_operator.py b/ultimatepython/syntax/walrus_operator.py new file mode 100644 index 0000000..c600127 --- /dev/null +++ b/ultimatepython/syntax/walrus_operator.py @@ -0,0 +1,118 @@ +""" +The walrus operator, also known as the assignment expression operator, +allows you to assign values to variables as part of an expression. This +feature was introduced in Python 3.8 through PEP 572. + +The walrus operator uses the := syntax and is particularly useful in +reducing redundancy when you need to compute a value and then use it +in a condition or comprehension. +""" + + +def main() -> None: + # Traditional approach: compute a value and check it separately + # Let's say we want to check if a string has more than 5 characters + text = "Hello, Python!" + length = len(text) + if length > 5: + assert length == 14 + + # With the walrus operator, we can assign and check in one expression. + # This is cleaner and avoids repeating the computation or storing + # intermediate variables unnecessarily + text2 = "Ultimate" + if (length2 := len(text2)) > 5: + # The walrus operator assigned len(text2) to length2 AND returned it + # for the comparison, all in the same line + assert length2 == 8 + + # The walrus operator is especially powerful in while loops. Here's a + # traditional approach that requires reading input twice + numbers_old = [] + n = int("5") # Simulating user input + while n != 0: + numbers_old.append(n) + n = int("0") # Simulating next input + assert numbers_old == [5] + + # With the walrus operator, we can assign and check in the loop condition. + # This is more concise and avoids the duplication of the input reading logic + numbers_new = [] + inputs = ["3", "7", "0"] # Simulating a sequence of inputs + index = 0 + + # Note: In a real scenario, you'd read from input() instead of a list + def get_next_input(): + nonlocal index + if index < len(inputs): + result = int(inputs[index]) + index += 1 + return result + return 0 + + while (num := get_next_input()) != 0: + # The walrus operator assigns get_next_input() to num and checks if it's not 0 + numbers_new.append(num) + assert numbers_new == [3, 7] + + # The walrus operator shines in list comprehensions when you need to + # compute a value once and reuse it. Without walrus, you'd either: + # 1. Compute the value multiple times (inefficient) + # 2. Use a for loop instead of comprehension (less elegant) + data = ["apple", "banana", "cherry", "date"] + + # Traditional approach: computing len() twice per item is wasteful + long_words_old = [word for word in data if len(word) > 5 and len(word) < 7] + assert long_words_old == ["banana", "cherry"] + + # With walrus operator: compute len() once and reuse the result + # The walrus operator assigns len(word) to word_len, which we can + # then use multiple times in the same comprehension + long_words_new = [word for word in data if 5 < (word_len := len(word)) < 7] + assert long_words_new == ["banana", "cherry"] + + # The walrus operator can be used in comprehensions to filter and transform + # data simultaneously. Here we square numbers but only keep those where + # the square is less than 50 + numbers = [3, 5, 7, 9, 11] + + # Without walrus: we'd need to compute the square twice or use a for loop + squares_old = [n * n for n in numbers if n * n < 50] + assert squares_old == [9, 25, 49] + + # With walrus: compute the square once, store it, and use it + # This is both more efficient and clearer about the intent + squares_new = [square for n in numbers if (square := n * n) < 50] + assert squares_new == [9, 25, 49] + + # The walrus operator works in set comprehensions too + # Let's find unique word lengths greater than 4 + words = ["hi", "hello", "world", "python", "code"] + + # With walrus operator in a set comprehension + long_lengths = {word_len for word in words if (word_len := len(word)) > 4} + assert long_lengths == {5, 6} + + # And in dictionary comprehensions as well + # Create a dict of words to their lengths, but only for words > 4 chars + word_length_map = {word: word_len for word in words if (word_len := len(word)) > 4} + assert word_length_map == {"hello": 5, "world": 5, "python": 6} + + # Important note: The walrus operator creates variables in the enclosing scope. + # This means variables assigned with := inside a comprehension are accessible + # outside of it (unlike traditional loop variables in comprehensions) + [result for x in [1, 2, 3] if (result := x * 2) > 2] + # The variable 'result' is now accessible here, with its last assigned value + assert result == 6 + + # The walrus operator can be nested, though this can reduce readability + # Use nesting sparingly and only when it genuinely improves the code + values = [1, 2, 3, 4, 5] + if (total := sum(doubled := [x * 2 for x in values])) > 15: + # Here 'doubled' is assigned inside the expression for 'total' + assert doubled == [2, 4, 6, 8, 10] + assert total == 30 + + +if __name__ == "__main__": + main() From cf8a201d1c489de4ee6dd71ac1d2890f87dd747a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 14 Nov 2025 11:37:01 +0000 Subject: [PATCH 03/18] Update remaining README files (Hindi, Portuguese-Brazil, Chinese-Traditional) Co-authored-by: huangsam <515617+huangsam@users.noreply.github.com> --- README.hi.md | 5 +++++ README.pt_br.md | 5 +++++ README.zh_tw.md | 5 +++++ 3 files changed, 15 insertions(+) diff --git a/README.hi.md b/README.hi.md index 9ef3302..d2d4836 100644 --- a/README.hi.md +++ b/README.hi.md @@ -69,11 +69,14 @@ print("Ultimate Python स्टडी गाइड") - कंडीशनल: [if | if-else | if-elif-else](ultimatepython/syntax/conditional.py) ( 🍰 ) - लूप: [for-loop | while-loop](ultimatepython/syntax/loop.py) ( 🍰 ) - फ़ंक्शन: [def | lambda](ultimatepython/syntax/function.py) ( 🍰 ) + - वॉलरस ऑपरेटर: [असाइनमेंट एक्सप्रेशन :=](ultimatepython/syntax/walrus_operator.py) ( 🤯 ) + - तर्क प्रवर्तन: [केवल स्थितीय / | केवल कीवर्ड *](ultimatepython/syntax/arg_enforcement.py) ( 🤯 ) 3. **डेटा संरचनाएँ** - लिसट: [लिसट ऑपरेशन्स](ultimatepython/data_structures/list.py) ( 🍰 ) - ट्यूपल: [ट्यूपल ऑपरेशन्स](ultimatepython/data_structures/tuple.py) - सेट: [सेट ऑपरेशन्स](ultimatepython/data_structures/set.py) - डिक्ट: [डिक्शनरी ऑपरेशन्स](ultimatepython/data_structures/dict.py) ( 🍰 ) + - डिक्शनरी यूनियन: [डिक्शनरी मर्ज | और |=](ultimatepython/data_structures/dict_union.py) ( 🤯 ) - संकलन: [लिसट | ट्यूपल | सेट | डिक्ट](ultimatepython/data_structures/comprehension.py) - स्ट्रिंग: [स्ट्रिंग ऑपरेशन्स](ultimatepython/data_structures/string.py) ( 🍰 ) - डेक: [डेक](ultimatepython/data_structures/deque.py) ( 🤯 ) @@ -102,6 +105,8 @@ print("Ultimate Python स्टडी गाइड") - नियमित अभिव्यक्ति: [search | findall | match | fullmatch](ultimatepython/advanced/regex.py) ( 🤯 ) - डेटा फ़ॉर्मेट: [json | xml | csv](ultimatepython/advanced/data_format.py) ( 🤯 ) - दिनांक और समय: [datetime | timezone](ultimatepython/advanced/date_time.py) ( 🤯 ) + - पैटर्न मिलान: [match | case](ultimatepython/advanced/pattern_matching.py) ( 🤯 ) + - अपवाद समूह: [ExceptionGroup | except*](ultimatepython/advanced/exception_groups.py) ( 🤯 ) ## अतिरिक्त संसाधन diff --git a/README.pt_br.md b/README.pt_br.md index c940387..131bf22 100644 --- a/README.pt_br.md +++ b/README.pt_br.md @@ -75,11 +75,14 @@ Existem duas maneiras de rodar os módulos: - Condicional: [if | if-else | if-elif-else](ultimatepython/syntax/conditional.py) ( 🍰 ) - Loop/Laço: [for-loop | while-loop](ultimatepython/syntax/loop.py) ( 🍰 ) - Função: [def | lambda](ultimatepython/syntax/function.py) ( 🍰 ) + - Operador morsa: [Expressões de atribuição :=](ultimatepython/syntax/walrus_operator.py) ( 🤯 ) + - Aplicação de argumentos: [Somente posicional / | Somente palavra-chave *](ultimatepython/syntax/arg_enforcement.py) ( 🤯 ) 3. **Estrutura de dados** - Lista: [Operações de lista](ultimatepython/data_structures/list.py) ( 🍰 ) - Tupla: [Operações de tuplas](ultimatepython/data_structures/tuple.py) - Conjunto: [Operações de conjuntos](ultimatepython/data_structures/set.py) - Dicionário: [Operações de dicionários](ultimatepython/data_structures/dict.py) ( 🍰 ) + - União de dicionários: [Fusão de dicionários | e |=](ultimatepython/data_structures/dict_union.py) ( 🤯 ) - Comprehension: [list | tuple | set | dict](ultimatepython/data_structures/comprehension.py) - String: [Operações de String](ultimatepython/data_structures/string.py) ( 🍰 ) - Deque: [deque](ultimatepython/data_structures/deque.py) ( 🤯 ) @@ -108,6 +111,8 @@ Existem duas maneiras de rodar os módulos: - Expressões regulares (regexp): [search | findall | match | fullmatch](ultimatepython/advanced/regex.py) ( 🤯 ) - Formato de dados: [json | xml | csv](ultimatepython/advanced/data_format.py) ( 🤯 ) - Datetime: [datetime | timezone](ultimatepython/advanced/date_time.py) ( 🤯 ) + - Correspondência de padrões: [match | case](ultimatepython/advanced/pattern_matching.py) ( 🤯 ) + - Grupos de exceções: [ExceptionGroup | except*](ultimatepython/advanced/exception_groups.py) ( 🤯 ) ## Recursos adicionais diff --git a/README.zh_tw.md b/README.zh_tw.md index 0187335..b118f3c 100644 --- a/README.zh_tw.md +++ b/README.zh_tw.md @@ -72,11 +72,14 @@ print("Ultimate Python 學習大綱") - 條件運算式:[if | if-else | if-elif-else](ultimatepython/syntax/conditional.py) ( 🍰 ) - 迴圈:[for迴圈 | while迴圈](ultimatepython/syntax/loop.py) ( 🍰 ) - 定義函式:[def | lambda](ultimatepython/syntax/function.py) ( 🍰 ) + - 海象運算子:[賦值表達式 :=](ultimatepython/syntax/walrus_operator.py) ( 🤯 ) + - 參數強制:[僅位置 / | 僅關鍵字 *](ultimatepython/syntax/arg_enforcement.py) ( 🤯 ) 3. **資料結構** - 列表:[列表操作](ultimatepython/data_structures/list.py) ( 🍰 ) - 元組:[元組操作](ultimatepython/data_structures/tuple.py) - 集合:[集合操作](ultimatepython/data_structures/set.py) - 字典:[字典操作](ultimatepython/data_structures/dict.py) ( 🍰 ) + - 字典聯合:[字典合併 | 和 |=](ultimatepython/data_structures/dict_union.py) ( 🤯 ) - 綜合:[list | tuple | set | dict](ultimatepython/data_structures/comprehension.py) - 字串:[字串操作](ultimatepython/data_structures/string.py) ( 🍰 ) - 雙端隊列:[deque](ultimatepython/data_structures/deque.py) ( 🤯 ) @@ -104,6 +107,8 @@ print("Ultimate Python 學習大綱") - 正規表示式:[search | findall | match | fullmatch](ultimatepython/advanced/regex.py) ( 🤯 ) - 數據格式:[json | xml | csv](ultimatepython/advanced/data_format.py) ( 🤯 ) - 日期時間: [datetime | timezone](ultimatepython/advanced/date_time.py) ( 🤯 ) + - 模式匹配:[match | case](ultimatepython/advanced/pattern_matching.py) ( 🤯 ) + - 例外群組:[ExceptionGroup | except*](ultimatepython/advanced/exception_groups.py) ( 🤯 ) ## 額外資源 From 49d20a449b3f193d2d75b8936fe749ac28a8f86c Mon Sep 17 00:00:00 2001 From: Samuel Huang Date: Fri, 14 Nov 2025 07:12:39 -0800 Subject: [PATCH 04/18] Update README.fr.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- README.fr.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.fr.md b/README.fr.md index d963678..0974dc3 100644 --- a/README.fr.md +++ b/README.fr.md @@ -95,8 +95,8 @@ Deux méthodes sont possibles :     - Conditionnelle : [if | if-else | if-elif-else](ultimatepython/syntax/conditional.py) ( 🍰 )     - Boucle : [for-loop | while-loop](ultimatepython/syntax/loop.py) ( 🍰 )     - Fonction : [def | lambda](ultimatepython/syntax/function.py) ( 🍰 ) - - Opérateur morse : [Expressions d\'affectation :=](ultimatepython/syntax/walrus_operator.py) ( 🤯 ) - - Application d\'arguments : [Positionnels uniquement / | Mots-clés uniquement *](ultimatepython/syntax/arg_enforcement.py) ( 🤯 ) + - Opérateur morse : [Expressions d'affectation :=](ultimatepython/syntax/walrus_operator.py) ( 🤯 ) + - Application d'arguments : [Positionnels uniquement / | Mots-clés uniquement *](ultimatepython/syntax/arg_enforcement.py) ( 🤯 ) 3. **Structures de données**     - Liste : [Opérations sur les listes](ultimatepython/data_structures/list.py) ( 🍰 ) From f837b2bbf525d20a814f91ccb27a7ffdc3102772 Mon Sep 17 00:00:00 2001 From: Samuel Huang Date: Fri, 14 Nov 2025 07:13:30 -0800 Subject: [PATCH 05/18] Update README.fr.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- README.fr.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.fr.md b/README.fr.md index 0974dc3..506550b 100644 --- a/README.fr.md +++ b/README.fr.md @@ -135,7 +135,7 @@ Deux méthodes sont possibles :     - Format de données : [json | xml | csv](ultimatepython/advanced/data_format.py) ( 🤯 )     - Date et heure : [datetime | timezone](ultimatepython/advanced/date_time.py) ( 🤯 ) - Correspondance de motifs : [match | case](ultimatepython/advanced/pattern_matching.py) ( 🤯 ) - - Groupes d\'exceptions : [ExceptionGroup | except*](ultimatepython/advanced/exception_groups.py) ( 🤯 ) + - Groupes d'exceptions : [ExceptionGroup | except*](ultimatepython/advanced/exception_groups.py) ( 🤯 ) ## Ressources supplémentaires From f813a712bad733e404af9f66aef2e69df662bc66 Mon Sep 17 00:00:00 2001 From: Samuel Huang Date: Fri, 14 Nov 2025 07:14:52 -0800 Subject: [PATCH 06/18] Update ultimatepython/data_structures/dict_union.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- ultimatepython/data_structures/dict_union.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ultimatepython/data_structures/dict_union.py b/ultimatepython/data_structures/dict_union.py index e6a01d7..a305682 100644 --- a/ultimatepython/data_structures/dict_union.py +++ b/ultimatepython/data_structures/dict_union.py @@ -175,7 +175,7 @@ def main() -> None: error_raised = False try: # This will fail because list is not a dict - invalid = dict22 | [("b", 2)] + dict22 | [("b", 2)] except TypeError: error_raised = True assert error_raised is True From 57e1eaf6572c3634f605d4dc54d3d3000243181c Mon Sep 17 00:00:00 2001 From: Samuel Huang Date: Fri, 14 Nov 2025 07:15:56 -0800 Subject: [PATCH 07/18] Update ultimatepython/advanced/exception_groups.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- ultimatepython/advanced/exception_groups.py | 1 - 1 file changed, 1 deletion(-) diff --git a/ultimatepython/advanced/exception_groups.py b/ultimatepython/advanced/exception_groups.py index b47c668..4bf2924 100644 --- a/ultimatepython/advanced/exception_groups.py +++ b/ultimatepython/advanced/exception_groups.py @@ -33,7 +33,6 @@ def raising_exception_groups(): assert isinstance(eg.exceptions[2], KeyError) return "Caught exception group" - return "Should not reach here" def handling_with_except_star(): From 3dc2e9f3e9bed89c29a60e340174155520a11861 Mon Sep 17 00:00:00 2001 From: Samuel Huang Date: Fri, 14 Nov 2025 07:28:31 -0800 Subject: [PATCH 08/18] Fix ruff linter warnings --- pyproject.toml | 5 ++ ultimatepython/advanced/exception_groups.py | 55 ++++---------------- ultimatepython/data_structures/dict_union.py | 48 +++-------------- ultimatepython/syntax/arg_enforcement.py | 9 +--- 4 files changed, 25 insertions(+), 92 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 914e24f..a3f389d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,8 @@ +[project] +name = "ultimate-python" +version = "1.0.0" +requires-python = ">= 3.11" + [tool.ruff] line-length = 160 diff --git a/ultimatepython/advanced/exception_groups.py b/ultimatepython/advanced/exception_groups.py index 4bf2924..f1e92f6 100644 --- a/ultimatepython/advanced/exception_groups.py +++ b/ultimatepython/advanced/exception_groups.py @@ -16,11 +16,7 @@ def raising_exception_groups(): exceptions. You create it with a message and a sequence of exceptions. """ # Create a simple exception group with multiple exceptions - exceptions = [ - ValueError("Invalid value"), - TypeError("Wrong type"), - KeyError("Missing key") - ] + exceptions = [ValueError("Invalid value"), TypeError("Wrong type"), KeyError("Missing key")] try: # Raise an ExceptionGroup containing multiple exceptions @@ -34,7 +30,6 @@ def raising_exception_groups(): return "Caught exception group" - def handling_with_except_star(): """Demonstrate the except* syntax for handling exception groups. @@ -44,11 +39,7 @@ def handling_with_except_star(): results = [] try: - exceptions = [ - ValueError("Invalid value 1"), - ValueError("Invalid value 2"), - TypeError("Wrong type 1") - ] + exceptions = [ValueError("Invalid value 1"), ValueError("Invalid value 2"), TypeError("Wrong type 1")] raise ExceptionGroup("Processing errors", exceptions) except* ValueError as eg: # This catches all ValueError instances from the group @@ -72,12 +63,7 @@ def handling_multiple_types(): results = [] try: - exceptions = [ - ValueError("Bad value"), - TypeError("Bad type"), - RuntimeError("Runtime issue"), - ValueError("Another bad value") - ] + exceptions = [ValueError("Bad value"), TypeError("Bad type"), RuntimeError("Runtime issue"), ValueError("Another bad value")] raise ExceptionGroup("Various errors", exceptions) except* ValueError as eg: # Handles all ValueErrors (there are 2) @@ -100,12 +86,7 @@ def nested_exception_groups(): unless you explicitly unwrap them. """ # Create a flat exception group with mixed exception types - exceptions = [ - ValueError("Value error 1"), - ValueError("Value error 2"), - TypeError("Type error 1"), - RuntimeError("Runtime error") - ] + exceptions = [ValueError("Value error 1"), ValueError("Value error 2"), TypeError("Type error 1"), RuntimeError("Runtime error")] eg = ExceptionGroup("Multiple errors", exceptions) @@ -134,13 +115,9 @@ def partial_handling(): try: try: - exceptions = [ - ValueError("Value error"), - TypeError("Type error"), - KeyError("Key error") - ] + exceptions = [ValueError("Value error"), TypeError("Type error"), KeyError("Key error")] raise ExceptionGroup("Mixed errors", exceptions) - except* ValueError as eg: + except* ValueError: # Only handle ValueError, leaving TypeError and KeyError results.append("Handled ValueError") # The other exceptions are automatically re-raised @@ -160,12 +137,7 @@ def filtering_exceptions(): The subgroup() method allows you to filter exceptions by type. """ - exceptions = [ - ValueError("Value 1"), - TypeError("Type 1"), - ValueError("Value 2"), - RuntimeError("Runtime 1") - ] + exceptions = [ValueError("Value 1"), TypeError("Type 1"), ValueError("Value 2"), RuntimeError("Runtime 1")] eg = ExceptionGroup("All errors", exceptions) @@ -192,6 +164,7 @@ def practical_concurrent_example(): This demonstrates a common use case: running multiple tasks where each can fail independently. """ + def process_user(user_id): """Simulate processing a user.""" if user_id == 1: @@ -303,11 +276,7 @@ def split_by_type(eg): type_errors = eg.subgroup(TypeError) return value_errors, type_errors - exceptions = [ - ValueError("Value 1"), - TypeError("Type 1"), - ValueError("Value 2") - ] + exceptions = [ValueError("Value 1"), TypeError("Type 1"), ValueError("Value 2")] eg = ExceptionGroup("Mixed", exceptions) values, types = split_by_type(eg) @@ -325,11 +294,7 @@ def split_by_type(eg): def simulate_async_failures(): """Simulate collecting errors from multiple async operations.""" # In real async code, you might use asyncio.gather with return_exceptions=True - task_errors = [ - TimeoutError("Task 1 timed out"), - ValueError("Task 2 invalid input"), - ConnectionError("Task 3 connection failed") - ] + task_errors = [TimeoutError("Task 1 timed out"), ValueError("Task 2 invalid input"), ConnectionError("Task 3 connection failed")] results = {"timeout": 0, "value": 0, "connection": 0} diff --git a/ultimatepython/data_structures/dict_union.py b/ultimatepython/data_structures/dict_union.py index a305682..735b973 100644 --- a/ultimatepython/data_structures/dict_union.py +++ b/ultimatepython/data_structures/dict_union.py @@ -98,36 +98,22 @@ def main() -> None: dict20 = {"city": "NYC", "scores": [85, 90, 95]} dict21 = {"active": True} person = dict19 | dict20 | dict21 - assert person == { - "name": "Alice", - "age": 30, - "city": "NYC", - "scores": [85, 90, 95], - "active": True - } + assert person == {"name": "Alice", "age": 30, "city": "NYC", "scores": [85, 90, 95], "active": True} # Practical use case: Configuration merging # Start with default configuration - default_config = { - "timeout": 30, - "retries": 3, - "debug": False, - "log_level": "INFO" - } + default_config = {"timeout": 30, "retries": 3, "debug": False, "log_level": "INFO"} # User provides custom configuration (partial) - user_config = { - "timeout": 60, - "debug": True - } + user_config = {"timeout": 60, "debug": True} # Merge configurations, user settings override defaults final_config = default_config | user_config assert final_config == { "timeout": 60, # Overridden by user - "retries": 3, # From default + "retries": 3, # From default "debug": True, # Overridden by user - "log_level": "INFO" # From default + "log_level": "INFO", # From default } # Practical use case: Building objects incrementally @@ -140,34 +126,16 @@ def main() -> None: # Add profile info with_profile = with_auth | {"bio": "Developer", "location": "USA"} - assert with_profile == { - "id": 1, - "type": "user", - "username": "john", - "email": "john@example.com", - "bio": "Developer", - "location": "USA" - } + assert with_profile == {"id": 1, "type": "user", "username": "john", "email": "john@example.com", "bio": "Developer", "location": "USA"} # Practical use case: Updating records with |= - user_record = { - "id": 100, - "name": "Jane", - "status": "active", - "login_count": 5 - } + user_record = {"id": 100, "name": "Jane", "status": "active", "login_count": 5} # Apply update from an external source update = {"status": "inactive", "login_count": 6, "last_login": "2024-01-15"} user_record |= update - assert user_record == { - "id": 100, - "name": "Jane", - "status": "inactive", - "login_count": 6, - "last_login": "2024-01-15" - } + assert user_record == {"id": 100, "name": "Jane", "status": "inactive", "login_count": 6, "last_login": "2024-01-15"} # The union operators only work with dictionaries # Attempting to use them with non-dict types raises TypeError diff --git a/ultimatepython/syntax/arg_enforcement.py b/ultimatepython/syntax/arg_enforcement.py index 6546cae..3c6154a 100644 --- a/ultimatepython/syntax/arg_enforcement.py +++ b/ultimatepython/syntax/arg_enforcement.py @@ -69,7 +69,7 @@ def keyword_with_defaults(*, x=5, y=3): When providing arguments, you must use the keyword names. """ - return x ** y + return x**y def complex_signature(a, b, /, c, d=10, *, e, f=20): @@ -230,12 +230,7 @@ def create_user(username, *, admin=False, active=True, send_email=False): callers must specify exactly what they're setting, improving readability and preventing accidental mistakes. """ - return { - "username": username, - "admin": admin, - "active": active, - "send_email": send_email - } + return {"username": username, "admin": admin, "active": active, "send_email": send_email} # Clear intent at call site user = create_user("john_doe", admin=True, send_email=True) From 7608c3f1b9f9af3ad1897abe42aeb06ab7d74bbe Mon Sep 17 00:00:00 2001 From: Samuel Huang Date: Fri, 14 Nov 2025 07:42:22 -0800 Subject: [PATCH 09/18] Fix coverage issues --- pyproject.toml | 5 +++-- ultimatepython/advanced/pattern_matching.py | 11 ++++++++++- ultimatepython/data_structures/dict_union.py | 4 ++-- ultimatepython/syntax/arg_enforcement.py | 18 +++++++++--------- ultimatepython/syntax/walrus_operator.py | 3 +++ 5 files changed, 27 insertions(+), 14 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index a3f389d..557956a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,7 +15,7 @@ ensure_newline_before_comments = true line_length = 160 [tool.coverage.run] -branch = true +# branch = true [tool.coverage.report] exclude_lines = [ @@ -29,5 +29,6 @@ fail_under = 80 omit = [ "venv/**", "runner.py", - "**/__init__.py" + "**/__init__.py", + "ultimatepython/advanced/exception_groups.py" ] diff --git a/ultimatepython/advanced/pattern_matching.py b/ultimatepython/advanced/pattern_matching.py index 0b66482..f331353 100644 --- a/ultimatepython/advanced/pattern_matching.py +++ b/ultimatepython/advanced/pattern_matching.py @@ -90,7 +90,7 @@ def analyze_sequence(data): # The *rest captures remaining elements return f"First: {first}, Rest: {rest}" case _: - return "Not a list" + return "Not a list" # pragma: no cover def process_command(command): @@ -200,6 +200,10 @@ def main() -> None: assert classify_http_status(200) == "OK" assert classify_http_status(404) == "Not Found" assert classify_http_status(999) == "Unknown Status" + assert classify_http_status(201) == "Created" + assert classify_http_status(400) == "Bad Request" + assert classify_http_status(401) == "Unauthorized" + assert classify_http_status(500) == "Internal Server Error" # Test sequence pattern matching with tuples assert process_point((0, 0)) == "Origin" @@ -247,6 +251,9 @@ def main() -> None: c2 = Circle(Point(5, 5), 3) assert describe_shape(c2) == "Circle at (5, 5) with radius 3" + # Test unknown shape + assert describe_shape("unknown") == "Unknown shape" + # Test dictionary pattern matching user1 = {"type": "user", "name": "Alice", "age": 30} assert process_json_data(user1) == "User Alice is 30 years old" @@ -295,6 +302,7 @@ def process_range(data): assert process_range([1, 5]) == "Valid range: [1, 5]" assert process_range([5, 1]) == "Invalid range: [5, 1]" + assert process_range("not a pair") == "Not a pair" # Nested pattern matching def analyze_nested(data): @@ -309,6 +317,7 @@ def analyze_nested(data): assert analyze_nested([["pair", 1, 2], ["pair", 3, 4]]) == "Two pairs: (1,2) and (3,4)" assert analyze_nested([["single", 42]]) == "Single value: 42" + assert analyze_nested("invalid") == "Unknown structure" # Pattern matching is particularly useful for parsing and handling # structured data like API responses, configuration files, or diff --git a/ultimatepython/data_structures/dict_union.py b/ultimatepython/data_structures/dict_union.py index 735b973..5f52c59 100644 --- a/ultimatepython/data_structures/dict_union.py +++ b/ultimatepython/data_structures/dict_union.py @@ -81,7 +81,7 @@ def main() -> None: assert combined2 == {"a": 1, "b": 2, "c": 3, "x": 30} # The union operator works with empty dictionaries - empty = {} + empty: dict[str, int] = {} dict16 = {"a": 1, "b": 2} assert empty | dict16 == {"a": 1, "b": 2} assert dict16 | empty == {"a": 1, "b": 2} @@ -143,7 +143,7 @@ def main() -> None: error_raised = False try: # This will fail because list is not a dict - dict22 | [("b", 2)] + dict22 | [("b", 2)] # type: ignore [operator] except TypeError: error_raised = True assert error_raised is True diff --git a/ultimatepython/syntax/arg_enforcement.py b/ultimatepython/syntax/arg_enforcement.py index 3c6154a..65d5ca8 100644 --- a/ultimatepython/syntax/arg_enforcement.py +++ b/ultimatepython/syntax/arg_enforcement.py @@ -98,7 +98,7 @@ def main() -> None: positional_error = False try: # This will fail because 'a' and 'b' are positional-only - positional_only(a=5, b=3) + positional_only(a=5, b=3) # type: ignore [call-arg] except TypeError: positional_error = True assert positional_error is True @@ -107,7 +107,7 @@ def main() -> None: positional_error2 = False try: # This will fail because 'b' is positional-only - positional_only(5, b=3) + positional_only(5, b=3) # type: ignore [call-arg] except TypeError: positional_error2 = True assert positional_error2 is True @@ -120,7 +120,7 @@ def main() -> None: keyword_error = False try: # This will fail because 'x' and 'y' are keyword-only - keyword_only(4, 5) + keyword_only(4, 5) # type: ignore [misc] except TypeError: keyword_error = True assert keyword_error is True @@ -136,7 +136,7 @@ def main() -> None: # But positional-only must be positional mixed_error = False try: - mixed_parameters(pos_only="first", pos_or_kw="second", kw_only="third") + mixed_parameters(pos_only="first", pos_or_kw="second", kw_only="third") # type: ignore [call-arg] except TypeError: mixed_error = True assert mixed_error is True @@ -144,7 +144,7 @@ def main() -> None: # And keyword-only must be keyword mixed_error2 = False try: - mixed_parameters("first", "second", "third") + mixed_parameters("first", "second", "third") # type: ignore [misc] except TypeError: mixed_error2 = True assert mixed_error2 is True @@ -156,7 +156,7 @@ def main() -> None: # Even with defaults, must use positional syntax positional_default_error = False try: - positional_with_defaults(a=5, b=20) + positional_with_defaults(a=5, b=20) # type: ignore [call-arg] except TypeError: positional_default_error = True assert positional_default_error is True @@ -170,7 +170,7 @@ def main() -> None: # Must still use keyword syntax even when providing defaults keyword_default_error = False try: - keyword_with_defaults(2, 3) + keyword_with_defaults(2, 3) # type: ignore [misc] except TypeError: keyword_default_error = True assert keyword_default_error is True @@ -195,7 +195,7 @@ def main() -> None: # But 'a' and 'b' must be positional complex_error = False try: - complex_signature(a=1, b=2, c=3, e=4) + complex_signature(a=1, b=2, c=3, e=4) # type: ignore [call-arg] except TypeError: complex_error = True assert complex_error is True @@ -203,7 +203,7 @@ def main() -> None: # And 'e' must be keyword (even though 'f' has a default) complex_error2 = False try: - complex_signature(1, 2, 3, 10, 4) + complex_signature(1, 2, 3, 10, 4) # type: ignore [misc] except TypeError: complex_error2 = True assert complex_error2 is True diff --git a/ultimatepython/syntax/walrus_operator.py b/ultimatepython/syntax/walrus_operator.py index c600127..733e3c0 100644 --- a/ultimatepython/syntax/walrus_operator.py +++ b/ultimatepython/syntax/walrus_operator.py @@ -55,6 +55,9 @@ def get_next_input(): numbers_new.append(num) assert numbers_new == [3, 7] + # Ensure the function returns 0 when no more inputs + assert get_next_input() == 0 + # The walrus operator shines in list comprehensions when you need to # compute a value once and reuse it. Without walrus, you'd either: # 1. Compute the value multiple times (inefficient) From f217dcb461cbf810f7dee489297d626fbabc7fa5 Mon Sep 17 00:00:00 2001 From: Samuel Huang Date: Fri, 14 Nov 2025 07:45:02 -0800 Subject: [PATCH 10/18] Help the coverage go to 100% --- ultimatepython/advanced/pattern_matching.py | 1 + 1 file changed, 1 insertion(+) diff --git a/ultimatepython/advanced/pattern_matching.py b/ultimatepython/advanced/pattern_matching.py index f331353..260146e 100644 --- a/ultimatepython/advanced/pattern_matching.py +++ b/ultimatepython/advanced/pattern_matching.py @@ -210,6 +210,7 @@ def main() -> None: assert process_point((0, 5)) == "Y-axis at y=5" assert process_point((3, 0)) == "X-axis at x=3" assert process_point((4, 7)) == "Point at (4, 7)" + assert process_point("invalid") == "Not a valid 2D point" # Test sequence pattern matching with lists assert analyze_sequence([]) == "Empty list" From 36d4361d23232a9536ea4eaeef8aab33a7d77517 Mon Sep 17 00:00:00 2001 From: Samuel Huang Date: Fri, 14 Nov 2025 07:49:22 -0800 Subject: [PATCH 11/18] Make pattern matching func more explicit --- ultimatepython/advanced/pattern_matching.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/ultimatepython/advanced/pattern_matching.py b/ultimatepython/advanced/pattern_matching.py index 260146e..52f946f 100644 --- a/ultimatepython/advanced/pattern_matching.py +++ b/ultimatepython/advanced/pattern_matching.py @@ -8,7 +8,7 @@ """ -def classify_number(value): +def classify_number(value) -> str: """Classify a number using pattern matching with literals. This demonstrates matching against specific literal values. @@ -25,7 +25,7 @@ def classify_number(value): return "other" -def classify_http_status(status): +def classify_http_status(status) -> str: """Classify HTTP status codes using pattern matching. This shows how pattern matching can make code more readable @@ -48,7 +48,7 @@ def classify_http_status(status): return "Unknown Status" -def process_point(point): +def process_point(point) -> str: """Process a point tuple using pattern matching with sequences. This demonstrates pattern matching against tuple structures @@ -70,7 +70,7 @@ def process_point(point): return "Not a valid 2D point" -def analyze_sequence(data): +def analyze_sequence(data) -> str: """Analyze sequences using pattern matching. This shows how to match lists with specific structures @@ -93,7 +93,7 @@ def analyze_sequence(data): return "Not a list" # pragma: no cover -def process_command(command): +def process_command(command) -> str: """Process commands using pattern matching with guards. Guards are if conditions that provide additional filtering @@ -134,7 +134,7 @@ def __init__(self, center, radius): self.radius = radius -def describe_shape(shape): +def describe_shape(shape) -> str: """Describe shapes using pattern matching with class patterns. This demonstrates matching against class instances and @@ -166,7 +166,7 @@ def describe_shape(shape): return "Unknown shape" -def process_json_data(data): +def process_json_data(data) -> str: """Process JSON-like dictionary data with pattern matching. This shows how to match against dictionary structures. @@ -275,7 +275,7 @@ def main() -> None: assert process_json_data(invalid) == "Invalid data" # Pattern matching with OR patterns - def check_value(val): + def check_value(val) -> str: match val: case 0 | 1 | 2: # Match any of these values @@ -291,7 +291,7 @@ def check_value(val): assert check_value(10) == "large" # Pattern matching with AS patterns (walrus-like capture) - def process_range(data): + def process_range(data) -> str: match data: case [x, y] as pair if x < y: # Capture the entire matched value with 'as' @@ -306,7 +306,7 @@ def process_range(data): assert process_range("not a pair") == "Not a pair" # Nested pattern matching - def analyze_nested(data): + def analyze_nested(data) -> str: match data: case [["pair", x, y], ["pair", a, b]]: # Match nested structure From bd41f5bbfb0b0c081695712ae48cb69436c62242 Mon Sep 17 00:00:00 2001 From: Samuel Huang Date: Fri, 14 Nov 2025 07:57:25 -0800 Subject: [PATCH 12/18] Move inner functions to outside scope --- ultimatepython/advanced/pattern_matching.py | 66 +++++++++++---------- 1 file changed, 36 insertions(+), 30 deletions(-) diff --git a/ultimatepython/advanced/pattern_matching.py b/ultimatepython/advanced/pattern_matching.py index 52f946f..73f8119 100644 --- a/ultimatepython/advanced/pattern_matching.py +++ b/ultimatepython/advanced/pattern_matching.py @@ -166,6 +166,42 @@ def describe_shape(shape) -> str: return "Unknown shape" +def analyze_nested(data) -> str: + """Analyze nested structures using pattern matching.""" + match data: + case [["pair", x, y], ["pair", a, b]]: + # Match nested structure + return f"Two pairs: ({x},{y}) and ({a},{b})" + case [["single", val]]: + return f"Single value: {val}" + case _: + return "Unknown structure" + + +def check_value(val) -> str: + """Check value using OR patterns.""" + match val: + case 0 | 1 | 2: + # Match any of these values + return "small" + case 3 | 4 | 5: + return "medium" + case _: + return "large" + + +def process_range(data) -> str: + """Process range data with AS patterns.""" + match data: + case [x, y] as pair if x < y: + # Capture the entire matched value with 'as' + return f"Valid range: {pair}" + case [x, y]: + return f"Invalid range: [{x}, {y}]" + case _: + return "Not a pair" + + def process_json_data(data) -> str: """Process JSON-like dictionary data with pattern matching. @@ -275,47 +311,17 @@ def main() -> None: assert process_json_data(invalid) == "Invalid data" # Pattern matching with OR patterns - def check_value(val) -> str: - match val: - case 0 | 1 | 2: - # Match any of these values - return "small" - case 3 | 4 | 5: - return "medium" - case _: - return "large" - assert check_value(0) == "small" assert check_value(2) == "small" assert check_value(3) == "medium" assert check_value(10) == "large" # Pattern matching with AS patterns (walrus-like capture) - def process_range(data) -> str: - match data: - case [x, y] as pair if x < y: - # Capture the entire matched value with 'as' - return f"Valid range: {pair}" - case [x, y]: - return f"Invalid range: [{x}, {y}]" - case _: - return "Not a pair" - assert process_range([1, 5]) == "Valid range: [1, 5]" assert process_range([5, 1]) == "Invalid range: [5, 1]" assert process_range("not a pair") == "Not a pair" # Nested pattern matching - def analyze_nested(data) -> str: - match data: - case [["pair", x, y], ["pair", a, b]]: - # Match nested structure - return f"Two pairs: ({x},{y}) and ({a},{b})" - case [["single", val]]: - return f"Single value: {val}" - case _: - return "Unknown structure" - assert analyze_nested([["pair", 1, 2], ["pair", 3, 4]]) == "Two pairs: (1,2) and (3,4)" assert analyze_nested([["single", 42]]) == "Single value: 42" assert analyze_nested("invalid") == "Unknown structure" From b3526017fe841bcc718ac60ba64df83f0fbb68a1 Mon Sep 17 00:00:00 2001 From: Samuel Huang Date: Fri, 14 Nov 2025 08:02:55 -0800 Subject: [PATCH 13/18] Remove exception_group from the listing --- README.de.md | 3 +- README.es.md | 1 - README.fr.md | 29 +- README.hi.md | 1 - README.ko.md | 3 +- README.md | 1 - README.pt_br.md | 1 - README.zh_tw.md | 1 - pyproject.toml | 5 +- ultimatepython/advanced/exception_groups.py | 324 -------------------- 10 files changed, 18 insertions(+), 351 deletions(-) delete mode 100644 ultimatepython/advanced/exception_groups.py diff --git a/README.de.md b/README.de.md index baf34bd..f1808e4 100644 --- a/README.de.md +++ b/README.de.md @@ -124,8 +124,7 @@ Es gibt zwei Möglichkeiten, die Module auszuführen: - Regular expression: [search | findall | match | fullmatch](ultimatepython/advanced/regex.py) ( 🤯 ) - Data format: [json | xml | csv](ultimatepython/advanced/data_format.py) ( 🤯 ) - Datetime: [datetime | timezone](ultimatepython/advanced/date_time.py) ( 🤯 ) - - Pattern matching: [match | case](ultimatepython/advanced/pattern_matching.py) ( 🤯 ) - - Exception groups: [ExceptionGroup | except*](ultimatepython/advanced/exception_groups.py) ( 🤯 ) + - Pattern Matching: [match | case](ultimatepython/advanced/pattern_matching.py) ( 🤯 ) ## Zusätzliche Ressourcen diff --git a/README.es.md b/README.es.md index 54d9374..4b3be9d 100644 --- a/README.es.md +++ b/README.es.md @@ -123,7 +123,6 @@ Hay dos maneras de ejecutar los módulos: - Formatos de datos: [json | xml | csv](ultimatepython/advanced/data_format.py) ( 🤯 ) - Fecha y hora: [datetime | timezone](ultimatepython/advanced/date_time.py) ( 🤯 ) - Coincidencia de patrones: [match | case](ultimatepython/advanced/pattern_matching.py) ( 🤯 ) - - Grupos de excepciones: [ExceptionGroup | except*](ultimatepython/advanced/exception_groups.py) ( 🤯 ) ## Recursos adicionales diff --git a/README.fr.md b/README.fr.md index 506550b..ffec3ae 100644 --- a/README.fr.md +++ b/README.fr.md @@ -37,21 +37,21 @@ grâce à ce langage. 🎓 Voici les principaux objectifs de ce guide : -🏆 **Servir de ressource** pour les débutants en Python qui préfèrent apprendre de manière pratique.  +🏆 **Servir de ressource** pour les débutants en Python qui préfèrent apprendre de manière pratique. Ce dépôt contient une collection de modules indépendants pouvant être exécutés dans un IDE comme [PyCharm](https://www.jetbrains.com/pycharm/) ou dans le navigateur via [Replit](https://replit.com/languages/python3). Même un simple terminal suffit pour exécuter les exemples. La plupart des lignes contiennent des commentaires détaillés -qui guident le lecteur pas à pas.  +qui guident le lecteur pas à pas. Les utilisateurs sont encouragés à modifier le code source à leur guise tant que les routines `main` ne sont pas supprimées et que les programmes [s’exécutent correctement](runner.py) après chaque modification. -🏆 **Servir de guide pur** pour ceux qui souhaitent revoir les concepts fondamentaux de Python.  +🏆 **Servir de guide pur** pour ceux qui souhaitent revoir les concepts fondamentaux de Python. Seules les [bibliothèques intégrées](https://docs.python.org/3/library/) sont utilisées afin de présenter les concepts sans dépendre de notions spécifiques à un domaine. Ainsi, les bibliothèques open-source populaires comme `sqlalchemy`, `requests` ou `pandas` -ne sont pas installées.  +ne sont pas installées. Cependant, lire le code source de ces frameworks est fortement recommandé si ton objectif est de devenir un véritable [Pythonista](https://www.urbandictionary.com/define.php?term=pythonista). @@ -61,24 +61,24 @@ si ton objectif est de devenir un véritable [![Run on Replit](https://replit.com/badge/github/huangsam/ultimate-python)](https://replit.com/github/huangsam/ultimate-python) Clique sur le badge ci-dessus pour lancer un environnement fonctionnel dans ton navigateur -sans avoir besoin d’installer Git ou Python localement.  +sans avoir besoin d’installer Git ou Python localement. Si ces outils sont déjà installés, tu peux cloner directement le dépôt. -Une fois le dépôt accessible, tu es prêt à apprendre à partir des modules indépendants.  +Une fois le dépôt accessible, tu es prêt à apprendre à partir des modules indépendants. Pour tirer le meilleur parti de chaque module, lis le code et exécute-le. Deux méthodes sont possibles : -1. Exécuter un seul module :  +1. Exécuter un seul module :   `python ultimatepython/syntax/variable.py` -2. Exécuter tous les modules :  +2. Exécuter tous les modules :   `python runner.py` ## Table des matières -📚 = Ressource externe  -🍰 = Sujet débutant  -🤯 = Sujet avancé  +📚 = Ressource externe +🍰 = Sujet débutant +🤯 = Sujet avancé 1. **À propos de Python**     - Vue d’ensemble : [Qu’est-ce que Python](https://github.com/trekhleb/learn-python/blob/master/src/getting_started/what_is_python.md) ( 📚, 🍰 ) @@ -135,13 +135,12 @@ Deux méthodes sont possibles :     - Format de données : [json | xml | csv](ultimatepython/advanced/data_format.py) ( 🤯 )     - Date et heure : [datetime | timezone](ultimatepython/advanced/date_time.py) ( 🤯 ) - Correspondance de motifs : [match | case](ultimatepython/advanced/pattern_matching.py) ( 🤯 ) - - Groupes d'exceptions : [ExceptionGroup | except*](ultimatepython/advanced/exception_groups.py) ( 🤯 ) ## Ressources supplémentaires -👔 = Ressource d’entretien  -🧪 = Exemples de code  -🧠 = Idées de projets  +👔 = Ressource d’entretien +🧪 = Exemples de code +🧠 = Idées de projets ### Dépôts GitHub diff --git a/README.hi.md b/README.hi.md index d2d4836..c148c2c 100644 --- a/README.hi.md +++ b/README.hi.md @@ -106,7 +106,6 @@ print("Ultimate Python स्टडी गाइड") - डेटा फ़ॉर्मेट: [json | xml | csv](ultimatepython/advanced/data_format.py) ( 🤯 ) - दिनांक और समय: [datetime | timezone](ultimatepython/advanced/date_time.py) ( 🤯 ) - पैटर्न मिलान: [match | case](ultimatepython/advanced/pattern_matching.py) ( 🤯 ) - - अपवाद समूह: [ExceptionGroup | except*](ultimatepython/advanced/exception_groups.py) ( 🤯 ) ## अतिरिक्त संसाधन diff --git a/README.ko.md b/README.ko.md index bcf4cfc..5f774ba 100644 --- a/README.ko.md +++ b/README.ko.md @@ -113,8 +113,7 @@ print("Ultimate Python 학습 가이드") - 정규식 : [search | findall | match | fullmatch](ultimatepython/advanced/regex.py) ( 🤯 ) - 데이터 포맷 : [json | xml | csv](ultimatepython/advanced/data_format.py) ( 🤯 ) - 날짜와 시간 : [datetime | timezone](ultimatepython/advanced/date_time.py) ( 🤯 ) - - 패턴 매칭 : [match | case](ultimatepython/advanced/pattern_matching.py) ( 🤯 ) - - 예외 그룹 : [ExceptionGroup | except*](ultimatepython/advanced/exception_groups.py) ( 🤯 ) + - 패턴 매칭: [match | case](ultimatepython/advanced/pattern_matching.py) ( 🤯 ) ## 추가 자료 diff --git a/README.md b/README.md index c027d22..e78c2e0 100644 --- a/README.md +++ b/README.md @@ -126,7 +126,6 @@ There are two ways of running the modules: - Data format: [json | xml | csv](ultimatepython/advanced/data_format.py) ( 🤯 ) - Datetime: [datetime | timezone](ultimatepython/advanced/date_time.py) ( 🤯 ) - Pattern matching: [match | case](ultimatepython/advanced/pattern_matching.py) ( 🤯 ) - - Exception groups: [ExceptionGroup | except*](ultimatepython/advanced/exception_groups.py) ( 🤯 ) ## Additional resources diff --git a/README.pt_br.md b/README.pt_br.md index 131bf22..93bd372 100644 --- a/README.pt_br.md +++ b/README.pt_br.md @@ -112,7 +112,6 @@ Existem duas maneiras de rodar os módulos: - Formato de dados: [json | xml | csv](ultimatepython/advanced/data_format.py) ( 🤯 ) - Datetime: [datetime | timezone](ultimatepython/advanced/date_time.py) ( 🤯 ) - Correspondência de padrões: [match | case](ultimatepython/advanced/pattern_matching.py) ( 🤯 ) - - Grupos de exceções: [ExceptionGroup | except*](ultimatepython/advanced/exception_groups.py) ( 🤯 ) ## Recursos adicionais diff --git a/README.zh_tw.md b/README.zh_tw.md index b118f3c..e24a545 100644 --- a/README.zh_tw.md +++ b/README.zh_tw.md @@ -108,7 +108,6 @@ print("Ultimate Python 學習大綱") - 數據格式:[json | xml | csv](ultimatepython/advanced/data_format.py) ( 🤯 ) - 日期時間: [datetime | timezone](ultimatepython/advanced/date_time.py) ( 🤯 ) - 模式匹配:[match | case](ultimatepython/advanced/pattern_matching.py) ( 🤯 ) - - 例外群組:[ExceptionGroup | except*](ultimatepython/advanced/exception_groups.py) ( 🤯 ) ## 額外資源 diff --git a/pyproject.toml b/pyproject.toml index 557956a..a3f389d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,7 +15,7 @@ ensure_newline_before_comments = true line_length = 160 [tool.coverage.run] -# branch = true +branch = true [tool.coverage.report] exclude_lines = [ @@ -29,6 +29,5 @@ fail_under = 80 omit = [ "venv/**", "runner.py", - "**/__init__.py", - "ultimatepython/advanced/exception_groups.py" + "**/__init__.py" ] diff --git a/ultimatepython/advanced/exception_groups.py b/ultimatepython/advanced/exception_groups.py deleted file mode 100644 index f1e92f6..0000000 --- a/ultimatepython/advanced/exception_groups.py +++ /dev/null @@ -1,324 +0,0 @@ -""" -Exception Groups allow you to raise and handle multiple exceptions -simultaneously. This is particularly useful for concurrent code where -multiple operations can fail independently. - -Exception Groups were introduced in Python 3.11 through PEP 654. They use -the ExceptionGroup class and the new except* syntax for handling multiple -exception types from a group. -""" - - -def raising_exception_groups(): - """Demonstrate how to raise exception groups. - - ExceptionGroup is a built-in exception type that contains multiple - exceptions. You create it with a message and a sequence of exceptions. - """ - # Create a simple exception group with multiple exceptions - exceptions = [ValueError("Invalid value"), TypeError("Wrong type"), KeyError("Missing key")] - - try: - # Raise an ExceptionGroup containing multiple exceptions - raise ExceptionGroup("Multiple errors occurred", exceptions) - except ExceptionGroup as eg: - # We can access the exceptions in the group - assert len(eg.exceptions) == 3 - assert isinstance(eg.exceptions[0], ValueError) - assert isinstance(eg.exceptions[1], TypeError) - assert isinstance(eg.exceptions[2], KeyError) - return "Caught exception group" - - -def handling_with_except_star(): - """Demonstrate the except* syntax for handling exception groups. - - The except* syntax allows you to handle specific exception types - from an ExceptionGroup while leaving others unhandled. - """ - results = [] - - try: - exceptions = [ValueError("Invalid value 1"), ValueError("Invalid value 2"), TypeError("Wrong type 1")] - raise ExceptionGroup("Processing errors", exceptions) - except* ValueError as eg: - # This catches all ValueError instances from the group - # Note: eg is still an ExceptionGroup, but only containing ValueErrors - assert len(eg.exceptions) == 2 - results.append("Caught ValueErrors") - except* TypeError as eg: - # This catches all TypeError instances from the group - assert len(eg.exceptions) == 1 - results.append("Caught TypeError") - - return results - - -def handling_multiple_types(): - """Handle different exception types from a group separately. - - You can have multiple except* blocks to handle different - exception types independently. - """ - results = [] - - try: - exceptions = [ValueError("Bad value"), TypeError("Bad type"), RuntimeError("Runtime issue"), ValueError("Another bad value")] - raise ExceptionGroup("Various errors", exceptions) - except* ValueError as eg: - # Handles all ValueErrors (there are 2) - results.append(f"ValueError count: {len(eg.exceptions)}") - except* TypeError as eg: - # Handles all TypeErrors (there is 1) - results.append(f"TypeError count: {len(eg.exceptions)}") - except* RuntimeError as eg: - # Handles all RuntimeErrors (there is 1) - results.append(f"RuntimeError count: {len(eg.exceptions)}") - - return results - - -def nested_exception_groups(): - """Exception groups can be nested. - - You can have an ExceptionGroup that contains other ExceptionGroups. - Note: nested ExceptionGroups are treated as single exceptions themselves - unless you explicitly unwrap them. - """ - # Create a flat exception group with mixed exception types - exceptions = [ValueError("Value error 1"), ValueError("Value error 2"), TypeError("Type error 1"), RuntimeError("Runtime error")] - - eg = ExceptionGroup("Multiple errors", exceptions) - - results = [] - - try: - raise eg - except* ValueError as eg: - # Catches all ValueErrors from the group - results.append(f"Caught {len(eg.exceptions)} ValueErrors") - except* TypeError as eg: - results.append(f"Caught {len(eg.exceptions)} TypeErrors") - except* RuntimeError as eg: - results.append(f"Caught {len(eg.exceptions)} RuntimeErrors") - - return results - - -def partial_handling(): - """Demonstrate partial handling of exception groups. - - If you don't handle all exceptions in a group, the unhandled ones - are re-raised as a new ExceptionGroup. - """ - results = [] - - try: - try: - exceptions = [ValueError("Value error"), TypeError("Type error"), KeyError("Key error")] - raise ExceptionGroup("Mixed errors", exceptions) - except* ValueError: - # Only handle ValueError, leaving TypeError and KeyError - results.append("Handled ValueError") - # The other exceptions are automatically re-raised - - except ExceptionGroup as eg: - # The re-raised group contains only the unhandled exceptions - assert len(eg.exceptions) == 2 - assert isinstance(eg.exceptions[0], TypeError) - assert isinstance(eg.exceptions[1], KeyError) - results.append("Caught remaining exceptions") - - return results - - -def filtering_exceptions(): - """Filter and process exceptions from a group. - - The subgroup() method allows you to filter exceptions by type. - """ - exceptions = [ValueError("Value 1"), TypeError("Type 1"), ValueError("Value 2"), RuntimeError("Runtime 1")] - - eg = ExceptionGroup("All errors", exceptions) - - # Get a subgroup containing only ValueErrors - value_errors = eg.subgroup(ValueError) - assert value_errors is not None - assert len(value_errors.exceptions) == 2 - - # Get a subgroup containing only TypeErrors - type_errors = eg.subgroup(TypeError) - assert type_errors is not None - assert len(type_errors.exceptions) == 1 - - # If no matching exceptions, subgroup returns None - key_errors = eg.subgroup(KeyError) - assert key_errors is None - - return "Filtering successful" - - -def practical_concurrent_example(): - """A practical example simulating concurrent operations. - - This demonstrates a common use case: running multiple tasks - where each can fail independently. - """ - - def process_user(user_id): - """Simulate processing a user.""" - if user_id == 1: - raise ValueError(f"Invalid user ID: {user_id}") - elif user_id == 3: - raise ConnectionError(f"Cannot connect for user: {user_id}") - return f"Processed user {user_id}" - - user_ids = [1, 2, 3, 4] - errors = [] - successes = [] - - # Simulate processing multiple users - for user_id in user_ids: - try: - result = process_user(user_id) - successes.append(result) - except Exception as e: - errors.append(e) - - # If there were errors, raise them all together - results = {"successes": len(successes), "errors": 0} - - if errors: - try: - raise ExceptionGroup("User processing errors", errors) - except* ValueError as eg: - results["errors"] += len(eg.exceptions) - except* ConnectionError as eg: - results["errors"] += len(eg.exceptions) - - return results - - -def exception_group_with_notes(): - """Exception groups work with exception notes (also new in Python 3.11). - - You can add contextual information to exceptions using add_note(). - """ - results = [] - - try: - exc1 = ValueError("Invalid input") - exc1.add_note("Occurred in data validation") - exc1.add_note("User input was: 'abc123'") - - exc2 = TypeError("Wrong type provided") - exc2.add_note("Expected int, got str") - - raise ExceptionGroup("Processing failed", [exc1, exc2]) - - except* ValueError as eg: - # The notes are preserved in the exception - exception = eg.exceptions[0] - assert hasattr(exception, "__notes__") - results.append("ValueError with notes caught") - - except* TypeError as eg: - exception = eg.exceptions[0] - assert hasattr(exception, "__notes__") - results.append("TypeError with notes caught") - - return results - - -def main() -> None: - # Test basic exception group raising and catching - result1 = raising_exception_groups() - assert result1 == "Caught exception group" - - # Test except* syntax - result2 = handling_with_except_star() - assert result2 == ["Caught ValueErrors", "Caught TypeError"] - - # Test handling multiple exception types - result3 = handling_multiple_types() - assert "ValueError count: 2" in result3 - assert "TypeError count: 1" in result3 - assert "RuntimeError count: 1" in result3 - - # Test nested exception groups - result4 = nested_exception_groups() - assert len(result4) == 3 - assert "Caught 2 ValueErrors" in result4 - assert "Caught 1 TypeErrors" in result4 - assert "Caught 1 RuntimeErrors" in result4 - - # Test partial handling - result5 = partial_handling() - assert result5 == ["Handled ValueError", "Caught remaining exceptions"] - - # Test filtering exceptions - result6 = filtering_exceptions() - assert result6 == "Filtering successful" - - # Test practical concurrent example - result7 = practical_concurrent_example() - assert result7["successes"] == 2 # Users 2 and 4 processed successfully - assert result7["errors"] == 2 # Users 1 and 3 had errors - - # Test exception groups with notes - result8 = exception_group_with_notes() - assert len(result8) == 2 - - # You can also split exception groups programmatically - def split_by_type(eg): - """Split an exception group into separate groups by type.""" - value_errors = eg.subgroup(ValueError) - type_errors = eg.subgroup(TypeError) - return value_errors, type_errors - - exceptions = [ValueError("Value 1"), TypeError("Type 1"), ValueError("Value 2")] - eg = ExceptionGroup("Mixed", exceptions) - values, types = split_by_type(eg) - - assert values is not None and len(values.exceptions) == 2 - assert types is not None and len(types.exceptions) == 1 - - # The derive() method creates a new ExceptionGroup with the same message - new_eg = eg.derive([RuntimeError("New error")]) - assert new_eg.message == eg.message - assert len(new_eg.exceptions) == 1 - assert isinstance(new_eg.exceptions[0], RuntimeError) - - # Exception groups are particularly valuable in async code - # where multiple coroutines can fail simultaneously - def simulate_async_failures(): - """Simulate collecting errors from multiple async operations.""" - # In real async code, you might use asyncio.gather with return_exceptions=True - task_errors = [TimeoutError("Task 1 timed out"), ValueError("Task 2 invalid input"), ConnectionError("Task 3 connection failed")] - - results = {"timeout": 0, "value": 0, "connection": 0} - - try: - raise ExceptionGroup("Async task failures", task_errors) - except* TimeoutError: - results["timeout"] += 1 - except* ValueError: - results["value"] += 1 - except* ConnectionError: - results["connection"] += 1 - - return results - - async_results = simulate_async_failures() - assert async_results["timeout"] == 1 - assert async_results["value"] == 1 - assert async_results["connection"] == 1 - - # Traditional exception handling vs exception groups: - # Traditional: You can only catch one exception at a time - # Exception groups: You can catch and handle multiple exceptions simultaneously - # This is crucial for modern concurrent and parallel programming patterns - - -if __name__ == "__main__": - main() From 7137aee19abc9793fd7a8e8c16cb6920d8560d77 Mon Sep 17 00:00:00 2001 From: Samuel Huang Date: Fri, 14 Nov 2025 08:14:58 -0800 Subject: [PATCH 14/18] Drop codecov thresholds to 90% --- codecov.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/codecov.yml b/codecov.yml index e62f4b9..ddee4a3 100644 --- a/codecov.yml +++ b/codecov.yml @@ -2,6 +2,6 @@ # https://docs.codecov.com/docs/commit-status coverage: status: - patch: + project: default: - target: 100% + target: 90% From 32208342ae7f131e6cb0e14ad7a2411898d0986d Mon Sep 17 00:00:00 2001 From: Samuel Huang Date: Fri, 14 Nov 2025 08:16:17 -0800 Subject: [PATCH 15/18] Keep coverage fail_under at 90 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index a3f389d..d44941d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,7 +25,7 @@ exclude_lines = [ "if __name__ == .__main__.:", "any\\(" ] -fail_under = 80 +fail_under = 90 omit = [ "venv/**", "runner.py", From bd14494b1b9b0307ab46ff683641dad3929c9db4 Mon Sep 17 00:00:00 2001 From: Samuel Huang Date: Fri, 14 Nov 2025 08:18:19 -0800 Subject: [PATCH 16/18] Make project and patch at 90 --- codecov.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/codecov.yml b/codecov.yml index ddee4a3..1ba8163 100644 --- a/codecov.yml +++ b/codecov.yml @@ -5,3 +5,6 @@ coverage: project: default: target: 90% + patch: + default: + target: 90% From 2b57a74ca2a37eafc2e98976d03d1f002d54cedb Mon Sep 17 00:00:00 2001 From: Samuel Huang Date: Fri, 14 Nov 2025 08:18:31 -0800 Subject: [PATCH 17/18] Keep pyproject at 80 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index d44941d..a3f389d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,7 +25,7 @@ exclude_lines = [ "if __name__ == .__main__.:", "any\\(" ] -fail_under = 90 +fail_under = 80 omit = [ "venv/**", "runner.py", From 4a7b5c3c13cd3bbd4e8d7544577e7992187791f0 Mon Sep 17 00:00:00 2001 From: Samuel Huang Date: Fri, 14 Nov 2025 08:28:13 -0800 Subject: [PATCH 18/18] Remove the project metadata --- pyproject.toml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index a3f389d..914e24f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,8 +1,3 @@ -[project] -name = "ultimate-python" -version = "1.0.0" -requires-python = ">= 3.11" - [tool.ruff] line-length = 160