Does the type annotation in a function not guarantee the type in Python 3?

Asked

Viewed 1,237 times

8

It is known that in Python it is not necessary to define the type when declaring a variable and this is interesting, however, when creating a function, I cannot restrict the types of attributes, so in doing this:

def x(a, b):
    return a + b

I can’t guarantee that I add two whole numbers. So, this happens:

print(x('f', 'a'))

I have as a result fa, and that’s not what I wanted. I read on the internet, that I can dictate the type of attribute like this:

def x(a: int, b: int):
    return a + b

but if I do this:

print(x('f', 'a'))

I have as a result fa likewise.
To solve the problem, I was using this:

def x(a, b):
    if type(a) != int or type(b) != int:
        raise ValueError

    return a + b

But this turns out to be very big, besides being gambiarra. Finally, this also ends up hindering polymorphism. How do I fix this?

  • What exactly do you want to do? If it is to ensure that the values are integer, you have already done so. Weak typing languages are thus by definition and mainly Python, where the main philosophy is: the developer knows what he is doing, so if the function should not receive a string, there should not exist in the code such a call.

  • @Andersoncarloswoss actually, this is just my resistance, which I got used to with C++, but how does polymorphism look if I don’t have the signatures?

1 answer

8


Yes - this is the expected and desired behavior of a Python function.

Possible typing tips with syntax def x(a: int, b: int)->int: - are just that: tips. There are some programs that check statically this is - before you run the program, if there is "type leak" - that is, if any point in your program calls this function with something that is not an integer. The official program for this is the mypy - but note that both the use of a type checker like mypy and doing something after it issues its warnings are completely optional.

The right practice is to ensure - through unit testing and integration testing - that your functions do what you want when they receive the right parameters.

As commented in the pergunra, Python is a language for "adults who consent to what they do" - so, if your function was created, and is documented as such, to work with integers and yet you call it strings, you must know what you are doing. (And to prevent this from happening unintentionally, there is type-hinting syntax and checkers like mypy).

What happens is that if you call the function with incompatible realmetne parameters, at some point, will be raised ma exception of TypeError in the body of the function (in this case, if you pass a number and a string, for example). Python being a dynamic language, this is caused at runtime - so the importance of having tests in your program.

Now, in exceptional cases - you see, it’s not supposed to be the rule - you can put type checkers at runtime. Python allows much more sophisticated mechanisms than the example of if type(a) == ... that you put in. But all these checks can do at runtime is ... raise an exception. An exception that would eventually happen naturally when the code tries to execute something impossible with the past parameters. With few exceptions - as in this case: the strings are concatenated. But again, if it were dis parameters that could not be summed with the "+" operator, it would make little difference to the program at runtime if the error happened right at the function input, or at the bottom line, where the sum operation is done.

On the other hand, this kind of thing is definitely not the role of the compiler of a dynamic language like Python. The language, like PHP Javascript, Ruby, simply does not need to reserve space beforehand when compiling the program for a single parameter type in the function. Nuances like C, Java, G are static - and the compile-time error happens not to be "nice and find possible flaws" - but why the incompatible parameter does not "fit" into the function call, already at compile time. Then, again, in Python there is the optional step of static typing checking with mypy.

Now, just to not close the answer without any concrete example, I’ll give you an example of how you could check the input and output parameters of a function without having to encode a if with an expression for each parameter.

One possible approach is with the use of a "Decorator" - this in Python is a function that takes another function as a parameter (the function that is "decorated"), and returns a third function - which via rule is the original "enveloped" function in some complementary code. There is a special syntax to apply decorators, which makes its use very practical.

So in this case, we can combine the typehints and create a developer that, when calling the function, it checks whether the parameters passed are in accordance with the types specified by type-hinting in the decorated function. In case of incompatible parameters, the Developer raises a Typeerror, otherwise proceed with the original call.

Python syntax allows a great deal of flexibility in how parameters are passed to a function - and create this Decorator to account for all possible ways, among named parameters, expanded parameters with "*" that took into account all possible forms of parameter passing would be reasonably complex - if you are actually going to use something like this, it is better to make use of the existing utility functions in the module inspect from the standard library. A developer that acts only on parameters passed by name however, can be quite simple. Type-hints are inside the attributo __annotations__ of the original function:

def typecheck(func):
   annotations = func.__annotations__
   def wrapper(*args, **kwargs):
       if args:
            raise TypeError("Only named parameters allowed")
       for name, value in kwargs.items():
           if not name in annotations:
               raise TypeError(f"Parameter {name}  does not exist")
           if not isinstance(value, annotations[name]):
               raise TypeError(f"Parameter {name} must be an instance of {annotations[name].__name__}")
       # all parameter type parameters succesful - perform original function call:
       return func(**kwargs)
   return wrapper

And at the interactive prompt:

In [16]: @typecheck
    ...: def soma(a: int, b: int):
    ...:     return a + b
    ...: 
In [17]: soma(a="f", b="a")
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
(...)

TypeError: Parameter a must be an instance of <class 'int'>

updating The questioner continued in the comments:

Okay, note, I know what I’m doing when programming, but, like, is polymorphism? I learned that to differentiate functions in a class, compiler checks your signature, ie the amount of parameters and their types. If I can’t tell the compiler what they are the types, as it will differentiate two functions with the same name and same amount of attributes, but with different behaviors?

Being a dynamic typing language, the classical concept of "polymorphism" does not exist in Python - what comes as arguments for any function or method are "Python objects".

In fact, no need to rewrite almost all methods because of trivial changes in objects passed as an advantage parameter. For example, any function made to perform calculations with basic operations on numeric types will work with integers, floats, complexes, fractions, Decimals, etc... (and of course, you can specify that you only want the type numbers.Number no type-hinting to encompass all numeric types, thanks to "inheritance adoption" mechanisms will work fine - but you do not need to limit your method if this is not necessary)

If you need different behaviors according to the typing of the incoming parameters, your options are as follows:

  1. Use a block if/elif/else to select the desired behavior
  2. Use the decorator functools.singledispatch - it allows, with the use of decorators, a form of polymorphism in separate functions based on the typing of first argument passed to a function - https://docs.python.org/3/library/functools.html#functools.singledispatch
  3. Rethink your design - it makes sense that you have a function with the same signature that does different things depending on the type of data?
    • for example, if you are working with interfaces in classes, and want some methods with the same name to be available in several different classes, the first parameter (self) will always be an object of the class itself - and in that case, the distinct implementation already naturally happens in the class itself. Polymorphism would only enter to distinguish the code from the second parameter onwards.
  4. Create a set of utilities and patterns between decorators and tables in dictionaries to have "personal" tools for polymorphism in your projects. This is very quiet and fun given the nature of the language
  • Okay, note, I know what I’m doing when programming, but what about polymorphism? I learned that to differentiate functions in a class, the compiler checks its signature, i.e., the number of parameters and their types. If I can’t tell the compiler what the types are, how it will differentiate two functions with the same name and same amount of attributes, but with different behaviors?

  • 1

    @Felipenascimento is a behavior of strong typing languages only. Since in Python a single variable can receive a value of several types, it makes no sense to define multiple functions. In this case you implement only one function and define its behavior according to the types passed by parameter or create multiple functions with different names.

  • 1

    @Andersoncarloswoss: the question went back to "up", so one comment - Python is a "strong typing" language. The feature you refer to is "static typing" (opposite to "dynamic typing", which is the case of Python). Python has strong and dynamic typing.

  • @jsbueno Well noted.

Browser other questions tagged

You are not signed in. Login or sign up in order to post.