Python’s Type Variables: A Numeric Type Odyssey
Discover Python's synergy of type variables and pattern matching for creating versatile and type-safe code. This article delves into enhancing Python's numeric flexibility, offering insights into crafting adaptable, robust functions.
Overview
In the previous article "Type Hints: Python’s Weapon Against Code Anarchy, ". We improved a Python function to make it type-safe but limited it to integers. However, there's no compelling reason to restrict ourselves to int
when Python can handle various numeric types, and the multiplication operation is defined for those types.
In this piece, we'll expand our function's capabilities to embrace integers and other numeric types like floats. We'll use Type Variables in Python to achieve this. Type Variables allow us to write more generic and flexible code, enabling functions to operate on a range of types instead of being confined to just one.
Introducing Type Variables to Our Code
Python supports multiple numeric types. In this short article, we will enhance the code from the previous article by adding support for float and complex numbers in a type-safe manner:
from typing import Callable, Optional, TypeVar, Union
# Define a type variable that can be an int, float, or complex
Numeric = int | float | complex
def multiply(a: Numeric, b: Numeric) -> Numeric:
return a * b
def wrapper(func: Callable[[Numeric, Numeric], Numeric]) -> Callable[[Numeric, Numeric], Optional[Numeric]]:
def inner(a: Numeric, b: Numeric) -> Optional[Numeric]:
if a == 0 or b == 0:
print("cannot multiply by zero")
return None # Returning 0 instead of None
else:
return func(a, b)
return inner
def process_numbers(a: Numeric, b: Numeric) -> Optional[Numeric]:
multiplier = wrapper(multiply)
match multiplier(a, b):
case result if result is not None:
print(f"Result of operation: {result}")
return result
case _: # This match all is required to convince the type checker the match is exhaustive
print("Multiplication by zero isn't allowed")
return None
# Example calls
result_int = process_numbers(5, 3) # Result of operation: 15
result_float = process_numbers(2.5, 3.0) # Result of operation: 7.5
result_complex = process_numbers(1 + 2j, 2 - 3j) # Result of operation: (8+1j)
This iteration of our Python code demonstrates significant advancements in handling different numeric types by defining Numeric
as a union of int
, float
, and complex
. The multiply
function becomes inherently more versatile and capable of processing different numeric data types. This enhancement extends the function's utility and showcases the adaptability of Python's type system.
To facilitate the testing with different values, we added the process_numbers function, where we call the wrapped multiply function and pattern match on it. We employed a wildcard catch-all case (case _:
) in the pattern-matching construct. This choice stems from a limitation in Python's handling of Optional
types. In Python, an Optional
type is essentially a union of a given type and None
(e.g., Optional[Numeric]
is equivalent to Union[Numeric, None]
). This means we do not have a constructor like Some
or Just
for explicitly handling cases with data.
The lack of a constructor for differentiating a non-None
value within an Optional
is not a big issue because the type we use with it will usually have its constructor. But in this example, the Numeric type is a Type Variable, which means we do not have a constructor for it.
We could match each type (int, float, complex) individually, use an OR pattern, or, as we did, use a guard to enter the inhabited case and a wildcard for the None branch. Using None instead will give us an error with mypy
because the type checker is not clever enough to understand all the covered cases.
Conclusion
Our exploration of Python's type hints led us to type variables. By integrating these with structural pattern matching, we've unlocked code flexibility and robustness. Type variables allow us to write versatile functions handling different types and be clear and precise in their operation.
Combined with pattern matching, this approach enables us to craft exhaustive and type-safe programs. Our code now accommodates various scenarios and maintains strict adherence to type expectations, ensuring reliability and reducing the potential for runtime errors.
As usual, you'll be able to find the code for this article in our GitHub presence.
If you've found value in our content and wish to delve deeper into Python, we invite you to subscribe. Your subscription supports our work and connects you with a community passionate about coding excellence.
Stay tuned for more insights into Python's powerful features.
Addendum: A Special Note for Our Readers
I decided to delay the introduction of subscriptions. You can read the full story here.
If you find our content helpful, there are several ways you can support us:
- The easiest way is to share our articles and links page on social media; it is free and helps us greatly.
- If you want a great experience during the Chinese New Year, I am renting my timeshare in Phuket. A five-night stay in this resort in Phuket costs 11,582 € on Expedia. I am offering it in USD at an over 40% discount compared to that price. I received the Year of the Snake in style.
- If your finances permit it, we are happy over any received donation. It helps us offset the site's running costs and an unexpected tax bill. Any amount is greatly appreciated:
- Finally, some articles have links to relevant goods and services; buying through them will not cost you more. And if you like programming swag, please visit the TuringTacoTales Store on Redbubble. Take a look. Maybe you can find something you like: