Internal and external function signatures

I want a function to have two signatures: an internal and an external one.

Here’s one way to do it:

from typing import overload

class A: ...
class A_(A): ...

def f1(x: int, a: A_, y: int): ...      # type: ignore
def f1(x: int, a: A, y: int):
    f1(x, a, y)     # type error expected

The # type: ignore is necessary because there’s only one overload. This check can’t be globally disabled in pyright (the type checker I’m currently using) without disabling other important checks.

Note that the type error on the last line is exactly what I want.

Why am I doing this? To force the programmer to give explicit permission to pass certain types to certain functions. For example, the error above would be avoided by doing something like this:

def f1(x: int, a: A_, y: int): ...      # type: ignore
def f1(x: int, a: A, y: int):
    f1(x, lift(a), y)

A better alternative to the overload approach would be a decorator:

def f1(x: int, a: A, y: int):
    f1(x, a, y)     # type error expected

Unfortunately, Python doesn’t let us do full-fledge type-level programming like TypeScript, so I don’t think this is a viable route.

I also tried playing with covariance, unions, etc… to get the same result without overloads or decorators, but I don’t think that’s possible.

That said, I’d be extremely grateful to anyone who can prove me wrong!

Is it possible to come up with a signature such that when a type “enters” a function it changes in such a way that it can’t “enter” the function a second time?

def f1(a: ???):
    f1(a)       # type error

Thank you for your time!

The easiest way to have multiple function signatures, is by writing multiple functions.

In this case, why not use a single, possibly nominally _private, ‘inner’ function (that accepts a single type, and handles the recursion), and one or more ‘external’ wrapper functions for each type? You could even build them from a third factory function, that accepts the actual type.

@overload is popular, but it’s a surprisingly simple decorator. All it does is store info for the type checker. It doesn’t make Python into a multiple dispatch language (it’s still single dispatch).

Therefore make it easier for the type checker, and explicitly call the function you want to call (and that you want it to check the signature of).

Sorry, but I don’t understand your suggestion. My @overload version works as intended. Its only problem is that it’s verbose and repetitive, so I’m looking for an alternative.

I also don’t understand your remark about @overload being simple. What’s important is that type checkers understand it. Are you able to create your own overload-like decorator and have type checkers understand it without touching the type checkers? Obviously no.

I’m saying the code is far more complicated than it needs to be, and you’re asking too much from the type system.

I disagree. I know what I’m doing. If I get a good enough result, I’ll share my little project with the community.

1 Like

Fair enough. I’d really like to check out your project. I’ve most likely failed to understand your question, but my current train of thought is:

You have a class and function with an arg restricted to that class.

class A: ...
def f1(x: int, a: A, y: int):
    # Something to stop infinite recursion
    f1(x, a, y) 

All well and good. Than you have a subclass of the first class:

class A_(A):

In Python type checking, at least as far as mypy is concerned f1(123, A_(), 456) is perfectly fine. A_() is still an A.

Nonetheless, explicit is better than implicit afterall, and perhaps it’s belt and braces, you then overload f1 to tell type checkers to support f1 called with A_:

def f1(x: int, a: A_, y: int): ...      # type: ignore

Again that’s fine. I just don’t understand why it’s necessary for subclasses of A given the original type signature of f1.

Here’s where I’m confused. As I understand it, perhaps wrongly, your question is now, “given the subclass and the overload, how do I get type checkers to flag the snippet below as a type mismatch”:

    f1(x, a, y)     # type error expected

Due to both A_ being a subclass and the overload, f1(x, A_(), y) is a perfectly valid function call, even within the function itself.

I deduced, again perhaps incorrectly, you either wanted to flag a type error on the call f1(x, A_(), y), within the f1 body, despite having told the type checker twice over that that call is perfectly fine. Or were approaching this like multiple dispatch, and want to restrict ‘each’ individual f1 (they’d be different functions in C++) to calling itself for the recursion. But unlike C++, by providing only one implementation.

1 Like

Sorry for the delay. I’ve been so immersed in my little project that I hadn’t noticed your reply.

Since I’ve got good enough results to publish my project, I’ll leave you in suspense. It will all make sense when you see it.

Now that the technical stuff is done, I only need to complete it, add some (type) tests, and write the documentation.

By the way, I test the types with assert_type and rely on pyright’s reportUnnecessaryTypeIgnoreComment to check the presence of expected type errors. I wish we had something like assert_type_error for that.

Here you go: