Type hinting in Python allows you to annotate the data type of variables and functions. While type hinting is not required for the dynamically typed language, this feature enables intelligent type suggestions, type checking with mypy, and improved code clarity. In a recent scenario, I encountered the need to type values from command line inputs. This led to the discovery of new features and limitations of Python’s type hinting.
Typing with Command-Line Input
Suppose we have a function that operates on user input from the command line. Consider the example below, where we aim to pluralize an English word like “pie” based on a given quantity:
python pluralize.py pie 2 # Desired output: "2 pies"
The corresponding Python code, which utilizes two TypeAlias
es to enhance type-checking, is provided below. The get_args
function returns a tuple of possible values for SingularWord
. We can simply check to see if the raw input from the command line matches one of our expected possibilities. You can copy the code and enter pluralize.py pie 2
to see that it will successfully run. However, this approach is not without its limitations.
from argparse import Namespace
from sys import argv
from typing import TypeAlias, Literal, Union, get_args
SingularWord: TypeAlias = Literal["pie", "pastry"]
PluralWord: TypeAlias = Literal["pies", "pastries"]
class PluralizeNamespace(Namespace):
word: SingularWord
quantity: int
def pluralize(word: SingularWord, quantity: int) -> str:
plural_map: dict = dict(zip(get_args(SingularWord), get_args(PluralWord)))
return f"{quantity} {plural_map[word] if quantity != 1 else word}"
raw_word: Union[SingularWord, str] = argv[1]
raw_num: str = argv[2]
if raw_word in get_args(SingularWord):
processed_word: SingularWord = raw_word
processed_num: int = int(raw_num)
print(pluralize(processed_word, processed_num))
else:
raise ValueError("Unknown word requested for pluralization")
Using argparse
to Regulate Types
When we check the types, an area of improvement is quickly identified. I use mypy to type check my code, and running mypy pluralize.py
in the terminal yields an error indicating that the type assignment is incompatible:
error: Incompatible types in assignment (expression has type “Union[Literal[‘pie’, ‘pastry’], str]”, variable has type “Literal[‘pie’, ‘pastry’]”) [assignment]
There are two reasons for this. First, the mypy reveal_type
function reveals that get_args returns Any. That effectively means we we cannot infer the types of values coming from get_args. Secondly, there is no way to convert raw_word
into a SingularWord
, since SingularWord
is just a type hint, and not a data type evaluated at runtime like int
.
To overcome this challenge, Python’s argparse
module can be used to regulate proper typing consistently with mypy. We start by defining the command arguments and using choices
to restrict the user’s input to options specified by SingularWord
.
from argparse import ArgumentParser, Namespace
from typing import get_args
# ... other code omitted for brevity
parser: ArgumentParser = ArgumentParser(description="Converts a word and quantity to plural statement")
parser.add_argument("word", choices=get_args(SingularWord))
parser.add_argument("quantity", type=int)
arguments: Namespace = parser.parse_args()
word_: SingularWord = arguments.word
quantity_: int = arguments.quantity
plural_statement: str = pluralize(word_, quantity_)
print(plural_statement)
The above code ensures that only valid word choices are permitted, and is consistent with mypy
’s standards. Now, you may be wondering why word_
is typed when it has to be one of the choices we defined. This relates to some of the limitations of python’s argument handling.
In short, arguments
is a Namespace
, so any values extracted from it will need to be typed. This is even true for arguments where we have specified the type, like quantity
. If you try using reveal_type(arguments.quantity)
, mypy will indicate that arguments.quantity
is actually an Any
type. With a little more coding, we can restrict this value to the exact type we want.
Namespace Typing
parse_args
has an argument called namespace
that we can use to insert custom namespaces. In this case, our custom namespace will have the exact typing we want.
from argparse import Namespace
SingularWord: TypeAlias = Literal["pie", "pastry"]
PluralWord: TypeAlias = Literal["pies", "pastries"]
class PluralizeNamespace(Namespace):
word: SingularWord
quantity: int
We simply need to specify that our custom namespace will be used in lieu of the generic one.
arguments: Namespace = parser.parse_args(namespace=PluralizeNamespace)
Now, if you try to reveal the type of arguments.quantity
and arguments.word
, they will be int
and SingularWord
, which is exactly what we want.
Conclusion
Typing hinting is an immensely helpful feature of python that powers mypy’s ability to statically check the types used in code. The practice becomes more complex when using command line arguments as input to functions, since the inputs are up to the user. argparse
is an excellent module for constraining the arguments to values usable by your code. When you combine ArgumentParser
with your own typed custom Namespace
the entire code can be completely typed.
Resources
- Source code : 3 files containing the examples used in this post.
pluralize.py
: Initial attemptpluralize_with_argparse.py
: Initial attempt with argparsepluralize_namespace_typing.py
: Final version of the code with custom namespace
- Python type hinting: Python’s official documentation on type hinting
- mypy: An optional static type checker for python
- argparse: Python builtin module for parsing command line arguments