Proposal for enhancing runtime type checking in Python using pattern matching and type hints in function definitions

Hello Pythonistas,

I hope this message finds you well. I am relatively new to Python programming and have been working on enhancing my skills. Recently, I came across an intriguing concept while reading Fluent Python, 2nd Edition by Luciano Ramalho.

In Chapter 2, “An Array of Sequences,” the book discusses pattern matching with sequences and mentions that we can make patterns more specific by adding type information. The type information in patterns acts as runtime type checks, which sparked some ideas for me.

First Idea:

What if we use pattern matching to achieve stronger type checking within functions?

Here’s an example:

def function(*args):
    match args:
        case [str(), list(), tuple()]:
            pass
        case _:
            raise TypeError("Function has been called with wrong arguments")

# Examples of usage
function(1)  # Raises TypeError
function('', [], ())  # Works fine
function('0', [1], (2))  # Raises TypeError
function('0', [1], (2,))  # Works fine

As you can see, using pattern matching with type information (as mentioned in the book), the interpreter raises an exception if the function is called with arguments of the wrong types.

Second Idea:

Could we make this runtime type checking part of the interpreter itself?
I am curious if it would be feasible for the interpreter to use type information in match/case scenarios and apply the same logic when type hints are used in function definitions.

I would love to hear your thoughts on these ideas and their potential feasibility in future Python releases.

Thank you for your time and consideration.

Best regards,

It might be feasible, but it will never be part of the core interpreter. There are various runtime type checkers, but one of the core promises of python type checking is that it will never become mandatory.

1 Like

It is not going to be mandatory!
it could be an optional feature like what is now, if anybody uses type hints (which are not mandatory) the type information could be used.

Thank you for sharing this, this indeed can be valuable for such applications.

However, python core interpreter does not do type checking. This is the job of 3rd party type checkers, such as Mypy.

1 Like

Dynamic (runtime) checking of the type (as well of value and interrelationships) of function argument objects has been done forever. Match-case statements can be and are used for this as an alternative to if-else statements with isinstance checks.

The example given is not something that one should normally write, as *args is intended for when a function does not know how many positionally passed arguments it will get, and should adjust to some range of possibilities, whereas function requires exactly 3. I prefer the following:

def function(s, l, t):
    match (s, l, t):
        case [str(), list(), tuple()]:
            return 'OK'
        case _:
            return 'Type mismatch'
    # s, l, t are directly available without disassembling 'args'.

function('', [], ())  # 'OK'
function('0', [1], (2))  # 'Type mismatch'
function('0', [1], (2,))  # 'OK'
function(1)  # Raises more informative builtin 'missing' message.
# TypeError: function() missing 2 required positional arguments: 'l' and 't'

Even more informative for debugging would be something like

    for arg, name, typ in ((s,'s',str), (l,'l',list), (t,'t',tuple)):
        if not isinstance(arg, type):
            raise TypeError(f'{name} must be {typ}')

But see next comment on second idea.

Second idea: Explicit runtime type checking. In general, this is a waste of time as a well-written, debugged program will not have calls with TypeErrors. For debugging, they can be caught either implicitly when used ('abc' ** 3 raises TypeError) or by static analyzers. User inputs must be type checked somehow, but a static analyzer will recognize the i defined by i = int(input(prompt)) is an int and check all it uses in calls. A static analyzer should also recognize that alist defined by alist = [float_expression for i in range(10_000_000)] is a list of floats without rechecking all 10_000_000 items every time alist is passed in every re-run of the program. The latter is hideously wasteful.

Note that annotations are not just for automated type-checking. The also help humans writing code in an IDE that displays type info.

I initially thought that there might be performance benefit, then I might have adapted it for tuple[str, int, ...] checking.

However, using match seems to be slower.