How to spell type signatures for default argument?

PR#7980 proposes a change to how type signatures are spelled for arguments with default values.

For example,

@njit("int16(int16, optional(int16), optional(int16))")
def foo(x, y=2, z=None):
    ...

Without the PR and without the type signature, the code currently uses Omitted types:

In [5]: @njit
   ...: def foo(x, y=2, z=None):
   ...:     if z is None:
   ...:         z = 3
   ...:     return x + y + z
   ...:

In [6]: foo(1)
Out[6]: 6

In [7]: foo(1, 2)
Out[7]: 6

In [8]: foo(1, 2, 3)
Out[8]: 6

In [9]: foo.signatures
Out[9]:
[(int64, omitted(default=2), omitted(default=None)),
 (int64, int64, omitted(default=None)),
 (int64, int64, int64)]

The PR use of optional(int16) for y=2 seems to change the meaning of the optional type.

Would the following spelling be better?

@njit("int16(int16, int16, optional(int16))")
def foo(x, y=2, z=None):
    ...

So, the optional type meaning is unchanged and Numba should just check for default arguments regardless of the type signature.

1 Like

This is discussed in today’s meeting and a feature request is created for the above proposed change: Type signature spelling for arguments with default values · Issue #8950 · numba/numba · GitHub

Hello sklam,
I just want to add a point related to your post which I was not aware of regarding optional parameters.
There is actually an additional 4th case hidden in your example.
Numba performs type inference during the compilation process to generate optimized code. When a function has optional parameters, Numba needs to create separate signatures to handle different combinations of provided and default values for those parameters.

In the case of foo, there are multiple possible combinations of provided and default values for the parameters x, y, and z:

  1. If no value is provided for y and z, their default values are used.
  2. If a value is provided for y but not z, y is explicitly set while z uses its default value.
  3. If values are provided for both y and z, both parameters are explicitly set.
  4. If z is explicitly set to None, it represents a specific case where the default value of z is overridden with None.
    The 4th case was new to me until today:
    => omitted(default=None) != none
    This increases the effort to provide and maintain code with explicit signatures if there are multiple variables that can be None in scenarios where you want to catch both cases were a parameter that is None by default is being skipped or passed through as None explicitly.

Here is your example again with the additional case:

from numba import jit

@jit()
def foo(x, y=2, z=None):
   z = z or 3
   return x + y + z

foo(x=1)
foo(x=1, y=2)
foo(x=1, y=2, z=3)
foo(x=1, y=2, z=None)

"""
foo.signatures
[(int64, omitted(default=2), omitted(default=None)),
 (int64, int64, omitted(default=None)),
 (int64, int64, int64),
 (int64, int64, none)]
"""

For functions with 2 parameters with default=None you get 6 explicit signatures.
(‘int64’, ‘int64’),
(‘int64’, ‘none’),
(‘int64’, ‘omitted(default=None)’),
(‘none’, ‘none’),
(‘none’, ‘omitted(default=None)’),
(‘omitted(default=None)’, ‘omitted(default=None)’)

For functions with 3 parameters with default=None you get 10 explicit signatures.
(‘int64’, ‘int64’, ‘int64’),
(‘int64’, ‘int64’, ‘none’),
(‘int64’, ‘int64’, ‘omitted(default=None)’),
(‘int64’, ‘none’, ‘none’),
(‘int64’, ‘none’, ‘omitted(default=None)’),
(‘int64’, ‘omitted(default=None)’, ‘omitted(default=None)’),
(‘none’, ‘none’, ‘none’),
(‘none’, ‘none’, ‘omitted(default=None)’),
(‘none’, ‘omitted(default=None)’, ‘omitted(default=None)’),
(‘omitted(default=None)’, ‘omitted(default=None)’, ‘omitted(default=None)’)

These examples should demonstrate the various combinations of provided and default values for optional parameters. It seems to be important to consider these combinations when working with functions that have multiple optional parameters to ensure the desired behavior.