API design: using reserved names as parameter names?

I wonder what opinions are here about using parameter names like type, min, max, id in an API.

I have a package where those names are intuitive and clear for many API functions, but using them makes the pylinter and many IDEs complain constantly.

Are there strong opinions on what good design of a library would imply: use those names and work around any clashes if necessary to make it more intuitive for users of the library, or come up with less intuitive names to avoid using reserved names?

1 Like

I wonder what opinions are here about using parameter names like
type, min, max, id in an API.

They’re not reserved, they’re builtins. If they were reserved then
Python would probably forbid you from using them. For example, “as” is
reserved - it is a Python language keyword.

I have a package where those names are intuitive and clear for many API functions, but using them makes the pylinter and many IDEs complain constantly.

Yeah, you’re shadowing well known names, leading to easy misuse.

Are there strong opinions on what good design of a library would imply:
use those names and work around any clashes if necessary to make it
more intuitive for users of the library, or come up with less intuitive
names to avoid using reserved names?

Typical recomendations come in two forms:

  • find another synonymous name

  • add a trailing underscore, eg:

    def f(x, type_):
    … use type_ …

Cheers,
Cameron Simpson cs@cskk.id.au

1 Like

Thanks for clearing this up!

What is the possible misuse?
I am not sure why I should make the API less intuitive when the risk is really low and when I am aware of it anyway - as far as I can see the risk is entirely on the side of the module implementer, not something that could affect the users of the module.
So if my module uses the parameter topy in the API, all I have to do is make sure that I do not use type(somevar) anywhere which is easy.

This is where I struggle because 1) there are simply no intuitive and good synonyms and 2) the trailing underscore is ugly as hell for users of the api, especially with keyword parameters, e.g findnext(a, type_="x") is just not as nice as findnext(a, type="x")

So I am trying to figure out what the balance between the cost and the benefits of following PEP rules / recommendations is here.

Once the library gets used by too many people, there will be no chance to reconsider.

1 Like

What is the possible misuse?

If the name doesn’t act like the builtin, people reading your
implementation may be confused. If the function/method is long, you
yourself might forget the special meaning this name has within the
function at some point. And, of course, you’re temporarily prevented
from easy access to the builtin itself during the function.

I am not sure why I should make the API less intuitive when the risk is really low and when I am aware of it anyway - as far as I can see the risk is entirely on the side of the module implementer, not something that could affect the users of the module.

Correct.

So if my module uses the parameter topy in the API, all I have to do is make sure that I do not use type(somevar) anywhere which is easy.

Yes, it is all easily addressable. English is relatively rich in
synonyms, so choosing an alternative name is sometimes easy. But there’s
no need for you to adopt a different name.

Your difficulties seemed to stem largely from linters complaining.
There’s no need to accept all recommendations from linters. For example,
I run a few linters, and turn off various complaints, see this:

https://hg.sr.ht/~cameron-simpson/css/browse/bin-cs/lint

Note the pycodestyle_ignore and pylint_disable settings. You can also
filter lint output to discard things, and some (eg pylint) lets you
insert special comments into your code to indicate that you’re well
aware that you’re doing something it might complain about, and are
perfectly fine with it. Example from my own code:

from cs.upd import print  # pylint: disable=redefined-builtin

You may well want to make your own “lint” script to run linters how you
want them; it saves much pain.

This is where I struggle because 1) there are simply no intuitive and good synonyms and 2) the trailing underscore is ugly as hell for users of the api, especially with keyword parameters, e.g findnext(a, type_="x") is just not as nice as findnext(a, type="x")

I agree about the ugliness.

So I am trying to figure out what the balance between the cost and the
benefits of following PEP rules / recommendations is here.

Once the library gets used by too many people, there will be no chance to reconsider.

Aye.

I myself have a few places where I’ve got a parameter named “type”.

In a number of places I qualify a parameter, eg:

def _make_random_Block(self, block_type=None, leaf_only=False):

where I’ve used “block_type” instead of “type”. In code where I’ve got a
few different kinds of types floating around using qualified names like
“block_type” is well worth it.

Finally: I am a huge advocate of APIs being easy to use and easy to
remember. If “type” is the most ergonomic name for your parameter, just
do it!

Cheers,
Cameron Simpson cs@cskk.id.au

1 Like

Just to be clear, I understand that you are talking about naming
function parameters the same as built-in functions:

def do_something(min, max, len=0, id=999):
    ...

The risk for the users of your API is negligible, except that they
might be tempted to write code like this:

len = 50  # create a local variable called "len"
result = do_something(1, 100, len, id=1234)  # works fine
print(len("Hello World"))

That will fail with a mysterious error that will probably confuse the
hell out of your users:

TypeError: 'int' object is not callable

This is called “shadowing” and while there are legitimate reasons to do
so, it is also confusing and annoying when it happens by accident.

As the developer of the do_something function, you too have to deal
with that shadowing: inside the function, it is not so easy to access
the built-in min, max, len and id functions. Even when you know you have
done it, it is easy to forget that you have, and try to use the
shadowed builtin and then be surprised by the error. Not that I have
ever done such a thing whistles nonchalantly

This accidental or inadvertant shadowing is why linters flag it with a
warning and most people try to avoid it.

But it’s not forbidden, and you will find examples of deliberate
shadowing in the stdlib. So long as you go into this with your eyes
open, and pay attention to what you are doing, I would say go for it.

On the other hand, it may be pretty easy to avoid the problem by
changing the parameter names:

  • instead of min and max, use minimum and maximum or low and high;

  • instead of type, use kind, variety, category, genre, species,
    breed, classification, grade, etc;

    (unfortunately you can’t use “class”, because that actually is a
    reserved word)

  • instead of id, use refnum, accountnum, etc;

  • instead of len, use length or size.

Given that this is an API used by people other than yourself, the
question you should be asking is how likely it is that they will be
confused by their own accidental shadowing of the builtins.

3 Likes

Thank you for that detailed answer, and thanks for making it easier for me to decide in favour of ease of use! :slight_smile:

Thank you for this great answer, I can see how this may sometimes tempt the users of the API to use identical variable names and thus increase the risk of their built-ins getting shadowed!
I guess it depends on the concrete situation in the end, in my case this is in the context of essentially a “mini-DSL” implemented as Python callable classes where the init parms can be something like “min” or “type” and those names are commonly used and easy understood. So I think in my concrete case I will stick with the expected names but make sure to add a warning in the API/user documentation.

1 Like