|
9 | 9 | These features give you fine-grained control over your function signatures. |
10 | 10 | """ |
11 | 11 |
|
| 12 | +from collections.abc import Callable |
| 13 | +from typing import Any |
| 14 | + |
12 | 15 |
|
13 | 16 | def traditional_function(a: int, b: int, c: int) -> int: |
14 | 17 | """A traditional function where all parameters can be passed either way. |
@@ -84,158 +87,87 @@ def complex_signature(a: int, b: int, /, c: int, d: int = 10, *, e: int, f: int |
84 | 87 | return a + b + c + d + e + f |
85 | 88 |
|
86 | 89 |
|
87 | | -def main() -> None: |
88 | | - # Traditional function: can be called either way |
89 | | - assert traditional_function(1, 2, 3) == 6 |
90 | | - assert traditional_function(a=1, b=2, c=3) == 6 |
91 | | - assert traditional_function(1, b=2, c=3) == 6 |
| 90 | +def distance(x1: float, y1: float, x2: float, y2: float, /) -> float: |
| 91 | + """Calculate distance between two points. |
92 | 92 |
|
93 | | - # Positional-only function: must use positional arguments |
94 | | - assert positional_only(5, 3) == 8 |
95 | | - |
96 | | - # Trying to use keyword arguments with positional-only parameters |
97 | | - # will raise a TypeError |
98 | | - positional_error = False |
99 | | - try: |
100 | | - # This will fail because 'a' and 'b' are positional-only |
101 | | - positional_only(a=5, b=3) # type: ignore [call-arg] |
102 | | - except TypeError: |
103 | | - positional_error = True |
104 | | - assert positional_error is True |
105 | | - |
106 | | - # You also can't mix positional and keyword for positional-only params |
107 | | - positional_error2 = False |
108 | | - try: |
109 | | - # This will fail because 'b' is positional-only |
110 | | - positional_only(5, b=3) # type: ignore [call-arg] |
111 | | - except TypeError: |
112 | | - positional_error2 = True |
113 | | - assert positional_error2 is True |
| 93 | + The parameter names here are somewhat arbitrary (could be p1_x, etc.) |
| 94 | + so we make them positional-only to give us flexibility to rename them |
| 95 | + without breaking existing code. |
| 96 | + """ |
| 97 | + return ((x2 - x1) ** 2 + (y2 - y1) ** 2) ** 0.5 |
114 | 98 |
|
115 | | - # Keyword-only function: must use keyword arguments |
116 | | - assert keyword_only(x=4, y=5) == 20 |
117 | 99 |
|
118 | | - # Trying to use positional arguments with keyword-only parameters |
119 | | - # will raise a TypeError |
120 | | - keyword_error = False |
121 | | - try: |
122 | | - # This will fail because 'x' and 'y' are keyword-only |
123 | | - keyword_only(4, 5) # type: ignore [misc] |
124 | | - except TypeError: |
125 | | - keyword_error = True |
126 | | - assert keyword_error is True |
| 100 | +def create_user(username: str, *, admin: bool = False, active: bool = True, send_email: bool = False) -> dict[str, object]: |
| 101 | + """Create a user with clear intent for optional parameters. |
127 | 102 |
|
128 | | - # Mixed parameters demonstrate all three types |
129 | | - result = mixed_parameters("first", "second", kw_only="third") |
130 | | - assert result == "first-second-third" |
| 103 | + Making admin, active, and send_email keyword-only ensures that |
| 104 | + callers must specify exactly what they're setting, improving |
| 105 | + readability and preventing accidental mistakes. |
| 106 | + """ |
| 107 | + return {"username": username, "admin": admin, "active": active, "send_email": send_email} |
131 | 108 |
|
132 | | - # The middle parameter can be passed either way |
133 | | - result2 = mixed_parameters("first", pos_or_kw="second", kw_only="third") |
134 | | - assert result2 == "first-second-third" |
135 | 109 |
|
136 | | - # But positional-only must be positional |
137 | | - mixed_error = False |
| 110 | +def assert_raises_type_error(fn: Callable[..., Any], *args: Any, **kwargs: Any) -> None: |
| 111 | + """Verify that calling fn with given arguments raises a TypeError.""" |
| 112 | + raised = False |
138 | 113 | try: |
139 | | - mixed_parameters(pos_only="first", pos_or_kw="second", kw_only="third") # type: ignore [call-arg] |
| 114 | + fn(*args, **kwargs) |
140 | 115 | except TypeError: |
141 | | - mixed_error = True |
142 | | - assert mixed_error is True |
| 116 | + raised = True |
| 117 | + assert raised is True |
143 | 118 |
|
144 | | - # And keyword-only must be keyword |
145 | | - mixed_error2 = False |
146 | | - try: |
147 | | - mixed_parameters("first", "second", "third") # type: ignore [misc] |
148 | | - except TypeError: |
149 | | - mixed_error2 = True |
150 | | - assert mixed_error2 is True |
151 | 119 |
|
152 | | - # Positional-only with defaults |
153 | | - assert positional_with_defaults(5) == 15 # Uses default b=10 |
154 | | - assert positional_with_defaults(5, 20) == 25 # Overrides b with 20 |
| 120 | +def main() -> None: |
| 121 | + # Traditional function: can be called either way |
| 122 | + assert traditional_function(1, 2, 3) == 6 |
| 123 | + assert traditional_function(a=1, b=2, c=3) == 6 |
| 124 | + assert traditional_function(1, b=2, c=3) == 6 |
155 | 125 |
|
156 | | - # Even with defaults, must use positional syntax |
157 | | - positional_default_error = False |
158 | | - try: |
159 | | - positional_with_defaults(a=5, b=20) # type: ignore [call-arg] |
160 | | - except TypeError: |
161 | | - positional_default_error = True |
162 | | - assert positional_default_error is True |
| 126 | + # Positional-only function: must use positional arguments |
| 127 | + assert positional_only(5, 3) == 8 |
| 128 | + # Arguments before / cannot be passed by keyword name |
| 129 | + assert_raises_type_error(positional_only, a=5, b=3) |
| 130 | + assert_raises_type_error(positional_only, 5, b=3) |
163 | 131 |
|
164 | | - # Keyword-only with defaults |
165 | | - assert keyword_with_defaults() == 125 # Uses defaults: 5^3 |
166 | | - assert keyword_with_defaults(x=2) == 8 # 2^3 |
167 | | - assert keyword_with_defaults(y=2) == 25 # 5^2 |
168 | | - assert keyword_with_defaults(x=3, y=4) == 81 # 3^4 |
| 132 | + # Keyword-only function: must use keyword arguments |
| 133 | + assert keyword_only(x=4, y=5) == 20 |
| 134 | + # Arguments after * cannot be passed positionally |
| 135 | + assert_raises_type_error(keyword_only, 4, 5) |
169 | 136 |
|
170 | | - # Must still use keyword syntax even when providing defaults |
171 | | - keyword_default_error = False |
172 | | - try: |
173 | | - keyword_with_defaults(2, 3) # type: ignore [misc] |
174 | | - except TypeError: |
175 | | - keyword_default_error = True |
176 | | - assert keyword_default_error is True |
| 137 | + # Mixed parameters demonstrate all three types |
| 138 | + assert mixed_parameters("first", "second", kw_only="third") == "first-second-third" |
| 139 | + assert mixed_parameters("first", pos_or_kw="second", kw_only="third") == "first-second-third" |
| 140 | + # Positional-only must be positional, keyword-only must be keyword |
| 141 | + assert_raises_type_error(mixed_parameters, pos_only="first", pos_or_kw="second", kw_only="third") |
| 142 | + assert_raises_type_error(mixed_parameters, "first", "second", "third") |
| 143 | + |
| 144 | + # Positional-only with defaults still requires positional passing |
| 145 | + assert positional_with_defaults(5) == 15 |
| 146 | + assert positional_with_defaults(5, 20) == 25 |
| 147 | + assert_raises_type_error(positional_with_defaults, a=5, b=20) |
| 148 | + |
| 149 | + # Keyword-only with defaults still requires keyword passing |
| 150 | + assert keyword_with_defaults() == 125 |
| 151 | + assert keyword_with_defaults(x=2) == 8 |
| 152 | + assert keyword_with_defaults(y=2) == 25 |
| 153 | + assert keyword_with_defaults(x=3, y=4) == 81 |
| 154 | + assert_raises_type_error(keyword_with_defaults, 2, 3) |
177 | 155 |
|
178 | 156 | # Complex signature: demonstrating all parameter types |
179 | | - # Minimal call with required params only |
180 | | - result3 = complex_signature(1, 2, 3, e=4) |
181 | | - assert result3 == 40 # 1+2+3+10(default)+4+20(default) |
182 | | - |
183 | | - # Providing all parameters |
184 | | - result4 = complex_signature(1, 2, 3, 4, e=5, f=6) |
185 | | - assert result4 == 21 # 1+2+3+4+5+6 |
186 | | - |
187 | | - # Middle parameter 'c' can be passed by keyword |
188 | | - result5 = complex_signature(1, 2, c=3, e=4) |
189 | | - assert result5 == 40 |
190 | | - |
191 | | - # Parameter 'd' can also be passed by keyword |
192 | | - result6 = complex_signature(1, 2, 3, d=15, e=4) |
193 | | - assert result6 == 45 # 1+2+3+15+4+20 |
194 | | - |
195 | | - # But 'a' and 'b' must be positional |
196 | | - complex_error = False |
197 | | - try: |
198 | | - complex_signature(a=1, b=2, c=3, e=4) # type: ignore [call-arg] |
199 | | - except TypeError: |
200 | | - complex_error = True |
201 | | - assert complex_error is True |
202 | | - |
203 | | - # And 'e' must be keyword (even though 'f' has a default) |
204 | | - complex_error2 = False |
205 | | - try: |
206 | | - complex_signature(1, 2, 3, 10, 4) # type: ignore [misc] |
207 | | - except TypeError: |
208 | | - complex_error2 = True |
209 | | - assert complex_error2 is True |
210 | | - |
211 | | - # Practical use case: Positional-only is great for functions where |
212 | | - # parameter names are not meaningful or may change |
213 | | - def distance(x1: float, y1: float, x2: float, y2: float, /) -> float: |
214 | | - """Calculate distance between two points. |
215 | | -
|
216 | | - The parameter names here are somewhat arbitrary (could be p1_x, etc.) |
217 | | - so we make them positional-only to give us flexibility to rename them |
218 | | - without breaking existing code. |
219 | | - """ |
220 | | - return ((x2 - x1) ** 2 + (y2 - y1) ** 2) ** 0.5 |
221 | | - |
| 157 | + assert complex_signature(1, 2, 3, e=4) == 40 |
| 158 | + assert complex_signature(1, 2, 3, 4, e=5, f=6) == 21 |
| 159 | + assert complex_signature(1, 2, c=3, e=4) == 40 |
| 160 | + assert complex_signature(1, 2, 3, d=15, e=4) == 45 |
| 161 | + assert_raises_type_error(complex_signature, a=1, b=2, c=3, e=4) |
| 162 | + assert_raises_type_error(complex_signature, 1, 2, 3, 10, 4) |
| 163 | + |
| 164 | + # Practical use case 1: Positional-only parameter renaming flexibility |
222 | 165 | assert abs(distance(0, 0, 3, 4) - 5.0) < 0.01 |
223 | 166 |
|
224 | | - # Practical use case: Keyword-only is great for boolean flags or |
225 | | - # optional parameters where the intent should be clear at call site |
226 | | - def create_user(username: str, *, admin: bool = False, active: bool = True, send_email: bool = False) -> dict[str, object]: |
227 | | - """Create a user with clear intent for optional parameters. |
228 | | -
|
229 | | - Making admin, active, and send_email keyword-only ensures that |
230 | | - callers must specify exactly what they're setting, improving |
231 | | - readability and preventing accidental mistakes. |
232 | | - """ |
233 | | - return {"username": username, "admin": admin, "active": active, "send_email": send_email} |
234 | | - |
235 | | - # Clear intent at call site |
| 167 | + # Practical use case 2: Keyword-only clarity for optional flags |
236 | 168 | user = create_user("john_doe", admin=True, send_email=True) |
237 | 169 | assert user["admin"] is True |
238 | | - assert user["active"] is True # Used default |
| 170 | + assert user["active"] is True |
239 | 171 | assert user["send_email"] is True |
240 | 172 |
|
241 | 173 |
|
|
0 commit comments