Yes - this is the expected and desired behavior of a function in Python.
The possible typing tips with the def x(a: int, b: int)->int:
syntax are just that: tips. There are some programs that check statically this is - before you run the program, if there is "type cast" - that is, if any point in your program calls that function with something other than an integer. The official program for this is mypy
- but realize that both using a typing checker like mypy and doing something after it issues its warnings are completely optional.
The correct practice is to ensure - by means of unit tests and integration tests - that your functions do what you want when they receive the correct parameters.
As was commented in the question, Python is a language for "adults who consent to what they do" - so if your function was created, and is documented like this, to work with integers and yet you call it with strings, you must know what you are doing. (And to prevent this from occurring unintentionally, there is the syntax of type-hinting and verifiers such as mypy.)
What happens is that if you call the function with parameters that are actually incompatible, at some point, the exception of
TypeError
will be raised 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, this is not to be the rule - you can actually put type-checkers at runtime. Python allows much more sophisticated mechanisms than the if type(a) == ...
example you put. But all that these checks can do at runtime is ... raise an exception. An exception that would happen naturally when the code tries to perform something impossible with the passed parameters. With a few exceptions - as in this case: strings are concatenated. But again, if they were 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 input of the function, or at the bottom line, where the sum operation.
On the other hand, this kind of thing is definitely not the compiler's role of a dynamic language like Python. The language, just like PHP Javascript, Ruby, simply does not need to reserve space beforehand when compiling the program for a single parameter type in the function. Lignes such as C, Java, G are static - and the compile-time error does not have to be "good and find possible faults" - but why the incompatible parameter does not "fit" the function call, compilation. So, again, in Python there is the optional step of static typing checking with mypy.
Now, just to not close the answer without any concrete examples, I'll give an example of how you could check the input and output parameters of a function without having to code a if
with an expression for each parameter. / p>
A possible approach is to use 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 is as a rule the original function "enveloped" in some complementary code. There is a special syntax for applying decorators, which makes it very practical to use.
So, in the case, we can combine typehints and create a decorator that,
when the function is called, it checks whether the passed parameters conform to the types specified by type-hinting in the decorated function. In case of incompatible parameters, the decorator raises a TypeError, otherwise proceed with the original call.
Python syntax provides a great deal of flexibility in how parameters are passed to a function - and create this decorator to handle all possible shapes, including named parameters, expanded parameters with "*" that take into account all the possible ways of parameter passing would be fairly complex - if you really want to use something like this, it's best to make use of the utility functions in the inspect
module of the default library. A decorator that only acts on parameters passed by name however, can be quite simple. Type-hints are within the __annotations__
attribute 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'>