Function alias proposal

Abstract

Python has legacy function names that experience has shown to be confusing to users. This idea proposes adding function aliases to Python to allow explicit, non-breaking renaming of functions.`

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

def sum alias add:

class AnimalShelter:

    def add_dog(self,name:str)->None:
        pass

    def add_canine alias add_dog:

This would cause the interpreter to treat a call sum(3,4) as identical to add(3,4). In this example, sum would be the old name that is replaced by add.

Motivation

The proposal to rename re.match to re.prefixmatch.

Rationale

While updating old APIs to user-friendly names is understandable, adding a new function that does exactly the same thing as the existing function substitutes one source of confusion for another. To the best of my knowledge, the proposed syntax is not currently valid, so no program’s existing behavior will be affected. The proposed soft keyword alias** usage is similar to the existing Url: TypeAlias = str. It is compact and clear. Pre Python 3.15 that uses the older aliased name will not be affected.

Using an explicit alias syntax will allow tools to easily replace old names with new ones, if desired.

Specification

def oldname alias newname:

equivalent to:

oldname =newname

It seems to me that this is proposing a new syntax to express something that is already common, in a more terse way, without providing any extra features/benefits?

20 Likes

About the colour of the shed:

  • Url: TypeAlias = str is written type Url = str in more recent versions of Python.
    That would suggest an equivalent def oldname = newname.
  • The trailing colon : is very confusing (to at least my eyes).
    In Python, a trailing colon introduces an indented block (e.g. class, if, with, and def), but that doesn’t apply here IIUC.
  • Which of the two names is old and which is new doesn’t really matter, I think. They’re equivalents, and whether add or sum is the old name isn’t immediately apparent (or even relevant) from the code.

Obviously, the colour may well be irrelevant if (as @dr_carlos said) the need isn’t clear.

However, the type syntax is not strictly equivalent to Url: TypeAlias = str.

  • It allows syntactic type parameters type Map[K, V] = dict[K, V].
  • It removes the need to import typing.TypeAlias.

See the section specifying the ‘Generic Type Alias’ in PEP 695.

1 Like

So it’s supposed to have a different semantics vs sum = add in tools (linters, code completion, etc.)?

Perhaps, it could be done with something like sum: typing.DeprecatedAlias = add without the need to extend the syntax, and with backwards compatibilty.

2 Likes

Yes. I didn’t know this works:

class AnimalShelter:

    def add_dog(self,name:str)->None:
        pass    

    add_canine = add_dog 

    def add_cat(self,name:str)->None:
        pass

I’d also want to replace the docstring for oldname with something like legacy alias for newname() along with the minimum required version needed to switch.

It would be neat if the aliases were in a machine readable enough format that some pyupgrade-like tool could do the migration. I’m not sure what’s the best way to support that though.

So what you want is a function (or class) that wraps the other one. You could build this in pure Python.

I think we need to think again about the motivations.

For the purpose of library function aliases I personally will prefer them to have distinguishable introspection characteristics, but not cause so big a semantic difference as functools wrappers do.

One thing about functions (and classes) is that they have dunders. Do the aliased versions have different __name__ and __qualname__ than, or be otherwise distinguishable from the original? If not, we could simply use plain old assignmnt statements, and there’d be lilttle point in this proposal.

A different __name__ or other attribute (for introspection) needs a different function object (though not necessarily a different code object). That needs a function consuming the old function, like

add_canine = copy_with_different_name(add_dog, 'add_canine')

Can be pure Python as @Rosuav said.

(Edit: un-smarted the quotes in the code snippet)

Yeah, I agree. If I had to do this often (I don’t think I’ve ever done this, let alone often) then I’d probably write a decorator which I can already do.

I’m not petitioning for changing anything here – merely pointing out that oldname = newname or any new syntax that effectively does the missing part of the problem.

This seems unnecessary as the assignment statement or a lambda or even a function achieves most, if not all, of what you want. Each method offers more capabilities from typing to a custom doc string to full replacement.

new_name = old_name
new_name = lambda *args* : old_name(*args*)
def new_name(*args*) : old_name(*args*)

And you can also fix up the doc string

new_name.\_\_doc__ = old_name . \_\_doc__  . replace("old_name", "new_name"
old_name.\_\_doc__ = "Original " +  old_name . \_\_doc__

You can also use functools.wraps to copy all the attributes of an existing function to another function. Though most type checkers don’t handle this very well

from functools import wraps

@wraps(sum)
def add(*args, **kwargs):
    return sum(*args, **kwargs)

At runtime add is now indistinguishable from sum (except for the fact that they’re technically different objects, so is and == checks will fail)

1 Like

In the case that you want == to succeed, perhaps think about making some alias(fn: Callable[P, R]) -> Callable[P, R]: ..., where you return a FunctionAlias instance, where FunctionAlias has a __call__ and __eq__ given. If we then we’re to make the alias function internally use some _get_function_alias(fn), the result of that could be cached (inf. LRU size), and for the first (only) cache miss for the given function, we could set the globals (inspect.currentframe().f_back.f_back.f_globals) in order to replace the original function object with a FunctionAlias too (whilst copying the original, so the functionality is not lost.

E.g. could we then do stuff like this:

def f1(...):
    # Do something
    ...

@alias(f1)
def f2(...):
    pass

@alias(f1)
def f3(...):
    pass

assert f1 is f2
assert f2 is f3
# Same works with `==`, if __eq__ is defined well