Function parameters and type hints
Python 3 added syntax for type hints. The mypy tool is one way to validate these type hints to be sure the hints and the code agree. All the examples shown in this book have been checked with the mypy tool.
This extra syntax for the hints is optional. It's not used at runtime and has no performance costs. If hints are present, tools like mypy can use them. The tool checks that the operations on the n parameter inside the function agree with the type hint about the parameter. The tool also tries to confirm that the return expressions both agree with the type hint. When an application has numerous function definitions, this extra scrutiny can be very helpful.
Getting ready
For an example of type hints, we'll look at some color computations. The first of these is extracting the Red, Green, and Blue values from the color codes commonly used in the style sheets for HTML pages. There are a variety of ways of encoding the values, including strings, integers, and tuples. Here are some of the varieties of types:
- A string of six hexadecimal characters with a leading #; for example, "#C62D42".
- A string of six hexadecimal characters without the extra #; for example, "C62D42".
- A numeric value; for example, 0xC62D42. In this case, we've allowed Python to translate the literal value into an internal integer.
- A three-tuple of R, G, and B numeric values; for example, (198, 45, 66).
Each of these has a specific type hint. For strings and numbers, we use the type name directly, str or int. For tuples, we'll need to import the Tuple type definition from the typing module.
The conversion from string or integer into three values involves two separate steps:
- If the value is a string, convert it into an integer using the int() function.
- For integer values, split the integer into three separate values using the >> and & operators. This is the core computation for converting an integer, hx_int, into r, g, and b:
r, g, b = (hx_int >> 16) & 0xFF, (hx_int >> 8) & 0xFF, hx_int & 0xFF.
A single RGB integer has three separate values that are combined via bit shifting. The red value is shifted left 16 bits, the green value is shifted left eight bits, and the blue value occupies the least-significant eight bits. A shift left by 16 bits is mathematically equivalent to multiplying by 216. Recovering the value via a right shift is similar to piding by 216. The >> operator does the bit shifting, while the & operator applies a "mask" to save a subset of the available bits.
How to do it…
For functions that work with Python's atomic types (strings, integers, floats, and tuples), it's generally easiest to write the function without type hints and then add the hints. For more complex functions, it's sometimes easier to organize the type hints first. Since this function works with atomic types, we'll start with the function's implementation:
- Write the function without any hints:
def hex2rgb(hx_int): if isinstance(hx_int, str): if hx_int [0] == "#": hx_int = int(hx_int [1:], 16) else: hx_int = int(hx_int, 16) r, g, b = (hx_int >> 16) & 0xff, (hx_int >> 8) & 0xff, hx_int & 0xff return r, g, b
- Add the result hint; this is usually the easiest way to do this. It's based on the return statement. In this example, the return is a tuple of three integers. We can use Tuple[int, int, int] for this. We'll need the Tuple definition from the typing module. Note the capitalization of the Tuple type hint, which is distinct from the underlying type object:
from typing import Tuple
- Add the parameter hints. In this case, we've got two alternative types for the parameter: it can be a string or an integer. In the formal language of the type hints, this is a union of two types. The parameter is described as Union[str, int]. We'll need the Union definition from the typing module as well.
Combining the hints into a function leads to the following definition:
def hex2rgb(hx_int: Union[int, str]) -> Tuple[int, int, int]:
if isinstance(hx_int, str):
if hx_int[0] == "#":
hx_int = int(hx_int[1:], 16)
else:
hx_int = int(hx_int, 16)
r, g, b = (hx_int >> 16)&0xff, (hx_int >> 8)&0xff, hx_int&0xff
return r, g, b
How it works…
The type hints have no impact when the Python code is executed. The hints are designed for people to read and for external tools, like mypy, to process.
When mypy is examining this block of code, it will confirm that the hx_int variable is always used as either an integer or a string. If inappropriate methods or functions are used with this variable, mypy will report the potential problem. The mypy tool relies on the presence of the isinstance() function to discern that the body of the first if statement is only used on a string value, and never used on an integer value.
In the r, g, b = assignment statement, the value for hx_int is expected to be an integer. If the isinstance(hx_int, str) value was true, the int() function would be used to create an integer. Otherwise, the parameter would be an integer to start with. The mypy tool will confirm the >> and & operations are appropriate for the expected integer value.
We can observe mypy's analysis of a type by inserting the reveal_type(hx_int) function into our code. This statement has function-like syntax; it's only used when running the mypy tool. We will only see output from this when we run mypy, and we have to remove this extra line of code before we try to do anything else with the module.
A temporary use of reveal_type() looks like this example:
def hex2rgb(hx_int: Union[int, str]) -> Tuple[int, int, int]:
if isinstance(hx_int, str):
if hx_int[0] == "#":
hx_int = int(hx_int[1:], 16)
else:
hx_int = int(hx_int, 16)
reveal_type(hx_int) # Only used by mypy. Must be removed.
r, g, b = (hx_int >> 16)&0xff, (hx_int >> 8)&0xff, hx_int&0xff
return r, g, b
The output looks like this when we run mypy on the specific module:
(cookbook) % mypy Chapter_03/ch03_r01.py
Chapter_03/ch03_r01.py:55: note: Revealed type is 'builtins.int'
The output from the reveal_type(hx_int) line tells us mypy is certain the variable will have an integer value after the first if statement is complete.
Once we've seen the revealed type information, we need to delete the reveal_type(hx_int) line from the file. In the example code available online, reveal_type() is turned into a comment on line 55 to show where it can be used. Pragmatically, these lines are generally deleted when they're no longer used.
There's more…
Let's look at a related computation. This converts RGB numbers into Hue-Saturation-Lightness values. These HSL values can be used to compute complementary colors. An additional algorithm required to convert from HSL back into RGB values can help encode colors for a web page:
- RGB to HSL: We'll look at this closely because it has complex type hints.
- HSL to complement: There are a number of theories on what the "best" complement might be. We won't look at this function. The hue value is in the range of 0 to 1 and represents degrees around a color circle. Adding (or subtracting) 0.5 is equivalent to a 180° shift, and is the complementary color. Offsets by 1/6 and 1/3 can provide a pair of related colors.
- HSL to RGB: This will be the final step, so we'll ignore the details of this computation.
We won't look closely at all of the implementations. Information is available at https://www.easyrgb.com/en/math.php if you wish to create working implementations of most of these functions.
We can rough out a definition of the function by writing a stub definition, like this:
def rgb_to_hsl(rgb: Tuple[int, int, int]) -> Tuple[float, float, float]:
This can help us visualize a number of related functions to be sure they all have consistent types. The other two functions have stubs like these:
def hsl_complement(hsl: Tuple[float, float, float]) -> Tuple[float, float, float]:
def hasl_to_rgb(hsl: Tuple[float, float, float]) -> Tuple[int, int, int]:
After writing down this initial list of stubs, we can identify that type hints are repeated in slightly different contexts. This suggests we need to create a separate type to avoid repetition of the details. We'll provide a name for the repeated type detail:
RGB = Tuple[int, int, int]
HSL = Tuple[float, float, float]
def rgb_to_hsl(color: RGB) -> HSL:
def hsl_complement(color: HSL) -> HSL:
def hsl_to_rgb(color: HSL) -> RGB:
This overview of the various functions can be very helpful for assuring that each function does something appropriate for the problem being solved, and has the proper parameters and return values.
As noted in Chapter 1, Numbers, Strings, and Tuples, Using NamedTuples to Simplify Item Access in Tuples recipe, we can provide a more descriptive set of names for these tuple types:
from typing import NamedTuple
class RGB(NamedTuple):
red: int
green: int
blue: int
def hex_to_rgb2(hx_int: Union[int, str]) -> RGB:
if isinstance(hx_int, str):
if hx_int[0] == "#":
hx_int = int(hx_int[1:], 16)
else:
hx_int = int(hx_int, 16)
# reveal_type(hx_int)
return RGB(
(hx_int >> 16)&0xff,
(hx_int >> 8)&0xff,
(hx_int&0xff)
)
We've defined a unique, new Tuple subclass, the RGB named tuple. This has three elements, available by name or position. The expectation stated in the type hints is that each of the values will be an integer.
In this example, we've included a reveal_type() to show where it might be useful. In the long run, once the author understands the types in use, this kind of code can be deleted.
The hex_to_rgb2() function creates an RGB object from either a string or an integer. We can consider creating a related type, HSL, as a named tuple with three float values. This can help clarify the intent behind the code. It also lets the mypy tool confirm that the various objects are used appropriately.
See also
- The mypy project contains a wealth of information. See https://mypy.readthedocs.io/en/latest/index.html for more information on the way type hints work.