Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ability to use Callable type alias when annotating functions #1641

Closed
neruson opened this issue Jun 3, 2016 · 18 comments
Closed

Ability to use Callable type alias when annotating functions #1641

neruson opened this issue Jun 3, 2016 · 18 comments

Comments

@neruson
Copy link

neruson commented Jun 3, 2016

If I have a type alias that is a Callable and a function that matches that type alias, it would be nice to annotate the function using just the type alias.

Example:

Validator = Callable[[Any], Optional[str]]

def check_string(val):
    # type: Validator
    ... # check if val is a string, return an error message if it isn't
@gvanrossum
Copy link
Member

But I ask myself, how would we do that in Python 3 using inline function annotations (PEP 3107)? I'm not sure this is a desirable feature.

@rwbarton
Copy link
Contributor

In a decorator you may want to give an inner function (that is going to be returned by the decorator) a type that is a type variable (if mypy supported that, which it doesn't yet). That has the same problem with regards to inline function annotation syntax. (Or if we had variadic type variables for abstracting over arbitrary function argument lists (python/typing#193) the same would apply to some extent.)

@PaperclipBadger
Copy link

Would a decorator be suitable for this? Something along the lines of

from typing import Callable, Optional, coerce

Validator = Callable[[Any], Optional[str]]

@coerce(Validator)
def check_string(val):
    ... # raise if ``val`` is invalid
    return val

coerce(type)(function) returns function. The decorator @coerce(type) indicates to the type checker that it should attempt to unify the type of the decorated function with type. Another example use case:

from typing import Callable, NamedTuple, coerce
import math

Constraint = Callable[[float], float]

class Clip(Constraint, NamedTuple):
    lower: float
    upper: float
    
    def __call__(self, x: float) -> float:
        return max(self.lower, min(self.upper, x))

@coerce(Constraint)
def softplus(x: float) -> float:
    return math.log(1 + math.exp(x))

@coerce(Constraint)
def null(x: float) -> float: 
    return x

Without coerce, there's no easy way for me to verify that softplus and null are Constraints.

@ilevkivskyi
Copy link
Member

Again, it looks like this is the case where @declared_type() PR #3291 would work as is, for example:

Validator = Callable[[Any], Optional[str]]

@declared_type(Validator)
def check_string(val):
    # some complex logic here
    ...

see the discussion in #2087

@Neppord
Copy link

Neppord commented Oct 10, 2017

I had this issue as well.

I wanted to use the type as a interface:

from typing import Optional, Callable, Tuple, TypeVar

T = TypeVar("T")
parser = Callable[[str], Optional[Tuple[str, T]]]


#  cant use parser[str] as type for take_one
def take_one(s):
    return s[1:], s[1]


def match(to_match: str) -> parser[str]:
    #  cant use parser[str] as type for p
    def p(s: str) -> Optional[Tuple[str, str]]:
        if s.startswith(to_match):
            return s[len(to_match):], to_match
        else:
            return None
    return p

and have a function with signature def or(first:parser[T], second: parser[T]) -> parser[T]: ... even using newType for parser (havent figured out if thats posible yet).

@nsgoetz
Copy link

nsgoetz commented Feb 12, 2018

I also had this issue - I have a bunch of super short handler that take a relatively large number of arguments, ideally had the argument typed as a single Tuple and I could alias the type to a variable. However, since tuple unpacking in function definitions is not supported in python 3, we have to move away from that system. This resulted in having to define each variable one one line since the type got too long, and thus some handlers with function definitions longer than their code. Being able to just alias the whole function type would solve that problem.

An alternative would be being able to declare an argument type alias similar to how I was defining a single Tuple type before:

HandlerArgType =  ArgAlias[Arg1Type, Arg2Type, Arg3Type, Arg4Type, Arg5Type, Arg6Type]
HandlerReturnType = SomeTypeHere # this already works

def handler_a(Arg1, Arg2, Arg3, Arg4, Arg5, Arg6):
    # type: (HandlerArgType) -> HandlerReturnType
    ....

@vispz
Copy link

vispz commented Apr 14, 2018

I did a cursory skim through the PEP but could not find this issue being addressed there. Could we comment the resolution here, as google anyway drops me here?

@gvanrossum
Copy link
Member

gvanrossum commented Apr 14, 2018

(Sorry, what I posted was for a different issue, so I deleted it here. What PEP did you skim?)

@vispz
Copy link

vispz commented Apr 14, 2018

@gvanrossum I went through PEP 3107.

@gvanrossum
Copy link
Member

What were you looking for? PEP 3107 doesn't mention Callable -- that was introduced in PEP 484, (which chronologically came after PEP 3107).

@vispz
Copy link

vispz commented Apr 15, 2018

Thank you for pointing me to PEP 484 :) and sorry for the delayed response. I wanted to make sure that I read the PEP in completion and not reply in haste.

From what I understand from PEP 484, the suggestion in this issue is not supported. To be a little more explicit, here is example of the use case

# in ./activations.py 

ActivationFunction = Callable[[float], float]

def sigmoid(x):
     # type: ActivationFunction
     return 1 / (1 + math.exp(-x))

def relu(x):
     # type: ActivationFunction
     return x if x > 0. else 0.

#-----------------------------
# in ./loader.py
import activations

def load_activation(name: str) -> ActivationFunction:
    return getattr(activations, name)

Currently this can be achieved by

class ActivationFunction(metaclass=abc.ABCMeta):
    """Base class for all activation functions.

    .. note::

        Please do not add any other method or store member fields unless absolutely
        necessary. Keep this class extremely slim moving logic into pure functions to
        aid testing.
    """

    @abc.abstractmethod
    def transform(x: float) -> float:
         raise NotImplementedError


class Sigmoid(ActivationFunction):

     def transform(x):
         return 1 / (1 + math.exp(-x))


class Relu(ActivationFunction):

     def transform(x):
         return x if x > 0. else 0.

Preferably, I would be able to use functions and use the typing system to check the function signature and the return type instead of creating classes. From at least what I gathered from the PEP-484, this is not supported use case. Please do correct me if I am wrong.

@PaperclipBadger
Copy link

Can you do this with variable annotations?

ActivationFunction = Callable[[Float], Float]

sigmoid: ActivationFunction
def sigmoid(x):
    return 1 / (1 + math.exp(-x))

@gvanrossum
Copy link
Member

the suggestion in this issue is not supported

That is correct. There is not even a finished design -- your proposal (use # type: ActivationFunction in the function body) does not mesh well with the preferred Python 3 syntax.

@vispz
Copy link

vispz commented Apr 16, 2018

@gvanrossum I see I see :)
@PaperclipBadger 👍
I do really like that syntax 💯It does not work currently, though.

# ./activation.py
from typing import Callable

import math


ActivationFunction = Callable[[float], float]


sigmoid: ActivationFunction
def sigmoid(x):
     return 1 / (1 + math.exp(-x))


relu: ActivationFunction
def relu(x):
     return x if x > 0. else 0.
$ mypy activation.py
activation.py:10: error: Name 'sigmoid' already defined on line 9
activation.py:15: error: Name 'relu' already defined on line 14

@stereobutter
Copy link

stereobutter commented Mar 13, 2019

For a language like python where everything is an object I argue that the syntax proposed by @PaperclipBadger is the expected behavior since

i: int
i = 1  # works

so why should this not work for functions (since they are also objects)?


Separating the type information from the actual implementation is also a thing in functional languages like Haskell where one would define a simple function that adds two integers like so:

add :: Integer -> Integer -> Integer
add x y =  x + y

From that point of view something like the following looks quite natural.

add: Callable[[int, int], int]
def add(a,b): 
    return a+b

In my opinion this is also a much nicer way of adding gradual typing to existing code bases compared to using stub files.

@gvanrossum
Copy link
Member

As long as we don't mess with the standard PEP 484 way to define functions, I think that this is a reasonable proposal. Perhaps someone can come up with a draft implementation as a PR? Then we could put it in mypy_extensions and write a PEP about it.

@captain-kark
Copy link

captain-kark commented Aug 6, 2019

I found myself looking at a few related issues attempting to limit the scope of input for the built in Decimal type.

Not sure if this is idiomatic, but it seems to work alright.

from decimal import Decimal as _Decimal, Context

StrictDecimalValue = typing.TypeVar(
    'StrictDecimalValue', str, _Decimal, "_StrictDecimal"
)
class _StrictDecimal(_Decimal):
    def __new__(
            cls, value: StrictDecimalValue,
            context: typing.Optional[Context] = None
    ) -> _Decimal:
        if not isinstance(value, (str, _Decimal)):
            raise ValueError("StrictDecimal must receive a str or Decimal type for value")
        if context is None:
            context = Context(prec=16, rounding=decimal.ROUND_UP)
        return _Decimal(value, context).normalize()

StrictDecimal = typing.TypeVar('StrictDecimal', _Decimal, _StrictDecimal, "Decimal")
Decimal = _StrictDecimal

ZERO = Decimal('0.0')
ONE = Decimal(1.0)  # error

If this proposal could consider this type of use-case, or if you know of a better way I can do this today, I'd appreciate it.

@JukkaL
Copy link
Collaborator

JukkaL commented Jan 29, 2020

I don't think that this is important enough for a non-standard mypy extension. If somebody wants to move this issue forward, discussion can continue on typing-sig@ or the typing issue tracker.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests