Designing type hints for optional parameters
This recipe combines the two previous recipes. It's common to define functions with fairly complex options and then add type hints around those definitions. For atomic types like strings and integers, it can make sense to write a function first, and then add type hints to the function.
In later chapters, when we look at more complex data types, it often makes more sense to create the data type definitions first, and then define the functions (or methods) related to those types. This philosophy of type first is one of the foundations for object-oriented programming.
Getting ready
We'll look at the two dice-based games, Craps and Zonk. In the Craps game, the players will be rolling two dice. In the Zonk game, they'll roll a number of dice, varying from one to six. The games have a common, underlying requirement to be able to create collections of dice. As noted in the Designing functions with optional parameters recipe, there are two broad strategies for designing the common function for both games; we'll rely on the General to Particular strategy and create a very general dice function.
How to do it…
- Define a function with the required and optional parameters. This can be derived from combining a number of examples. Or, it can be designed through careful consideration of the alternatives. For this example, we have a function where one parameter is required and one is optional:
def dice(n, sides=6): return tuple(random.randint(1, sides) for _ in range(n))
- Add the type hint for the return value. This is often easiest because it is based on the return statement. In this case, it's a tuple of indefinite size, but all the elements are integers. This is represented as Tuple[int, ...]. (... is valid Python syntax for a tuple with an indefinite number of items.)
- Add required parameter type hints. The parameter n must be an integer, so we'll replace the simple n parameter with n: int to include a type hint.
- Add optional parameter type hints. The syntax is more complex for these because we're inserting the hint between the name and the default value. In this case, the sides parameter must also be an integer, so we'll replace sides = 6 with sides: int = 6.
Here's the final definition with all of the type hints included. We've changed the name to make it distinct from the dice() example shown previously:
def dice_t(n: int, sides: int = 6) -> Tuple[int, ...]:
return tuple(random.randint(1, sides) for _ in range(n))
The syntax for the optional parameter contains a wealth of information, including the expected type and a default value.
Tuple[int, …], as a description of a tuple that's entirely filled with int values, can be a little confusing at first. Most tuples have a fixed, known number of items. In this case, we're extending the concept to include a fixed, but not fully defined number of items in a tuple.
How it works…
The type hint syntax can seem unusual at first. The hints can be included wherever variables are created:
- Function (and class method) parameter definitions. The hints are right after the parameter name, separated by a colon. As we've seen in this recipe, any default value comes after the type hint.
- Assignment statements. We can include a type hint after the variable name on the left-hand side of a simple assignment statement. It might look like this:
Pi: float = 355/113
Additionally, we can include type hints on function (and class method) return types. The hints are after the function definition, separated by a ->. The extra syntax makes them easy to read and helpful for a person to understand the code.
The type hint syntax is optional. This keeps the language simple, and puts the burden of type checking on external tools like mypy.
There's more…
In some cases, the default value can't be computed in advance. In other cases, the default value would be a mutable object, like a list, which we don't want to provide in the parameter definitions.
Here, we'll look at a function with very complex default values. We're going to be simulating a very wide domain of games, and our assumptions about the number of dice and the shape of the dice are going to have to change dramatically.
There are two fundamental use cases:
- When we're rolling six-sided dice, the default number of dice is two. This fits with two-dice games like Craps. If we call the function with no argument values, this is what we'd like to happen. We can also explicitly provide the number of dice in order to support multi-dice games.
- When we're rolling other dice, the default number of dice changes to one. This fits with games that use polyhedral dice of four, eight, twelve, or twenty sides. It even fits with irregular dice with ten sides.
These rules will dramatically change the way default values need to be handled in our dice() and dice_t() functions. We can't trivially provide a default value for the number of dice. A common practice is to provide a special value like None, and compute an appropriate default when the None value is provided.
The None value also expands the type hint requirement. When we can provide a value for an int or None, this is effectively Union[None, int]. The typing module lets us use Optional[int] for values for which None is a possibility:
from typing import Optional, Tuple
def polydice(n: Optional[int] = None, sides: int = 6) -> Tuple[int, ...]:
if n is None:
n = 2 if sides == 6 else 1
return tuple(random.randint(1, sides) for _ in range(n))
In this example, we've defined the n parameter as having a value that will either be an integer or None. Since the actual default value depends on other arguments, we can't provide a simple, fixed default in the function definition. We've used a default value of None to show the parameter is optional.
Here are four examples of using this function with a variety of argument values:
>>> random.seed(113)
>>> polydice()
(1, 6)
>>> polydice(6)
(6, 3, 1, 4, 5, 3)
>>> polydice(sides=8)
(4,)
>>> polydice(n=8, sides=4)
(4, 1, 1, 3, 2, 3, 4, 3)
In the first example, neither the n nor sides parameters were provided. In this case, the value used for n was two because the value of sides was six.
The second example provides a value for the n parameter. The expected number of six-sided dice were simulated.
The third example provides a value for the sides parameter. Since there's no value for the n parameter, a default value for the n parameter was computed based on the value of the sides parameter.
The fourth example provides values for both the n and the sides parameters. No defaults are used here.
See also
- See the Using super flexible keyword parameters recipe for more examples of how parameters and defaults work in Python.