Python function attributes – the proper guide

Please note that today’s article is kindly sponsored by Reboot Motion. Totally go to their website and check them out!

Seeming as the Internet is lacking in these kinds of resources. I endeavored to do some research of mine. Basically I was looking to writing a class that would either catch an error and re-raise it or just let it go, or re-raise it as a different class. That’s where I stumbled upon Python function attributes.

Furthermore, let’s clarify a few things each time I refer to something as it means the following:

  • keyword-only argument – ie. argument defined in such a way:
def test(a, b, *, c=2, d=3):
   pass

In this case both c and d are to be called keyword-only argument, because you can pass them only via a keyword.

As you might all know Python’s functions are objects too, and have some interesting attributes of their own, namely

  • __doc__ – this contains the entire docstring of the function
  • __annotations__ – this contains the mapping between the argument name and it’s type annotation. Special case should go to the argument name return which you can’t use because it’s a reserved Python keyword, to denote what type will our function return
  • __defaults__ – you’re at loss here. Python will provide you this as a tuple of values, that are to be placed right-side in the expression, such as this:
>> def test(a, b=2, c=3):
>>  pass
>> print(test.__defaults__)
(2, 3)

So you’re more likely to call your function like this:

known_number_of_arguments = (2, )
test(*known_number_of_arguments, *test.__defaults__)
  • __kwdefaults__ – you’re at much larger luck there. If you require any of your arguments to be passed in by name, such as by writing in this style (it might as well be None if empty):
>> def test(a: int, b: str = 3, c = (..., ), *, d: int = 2) -> int:
>>    pass
>> print(test.__kwdefaults__)
{{'d': 2}
  • __code__ – boy, this is where the game gets really have. Code represents a Python bytecode compiled object for this function, and it happens to have some really cool attributes:
    • co_argcount – number of arguments WITHOUT keyword only arguments
      co_kwonlyargcount – number of keyword only arguments for this function
    • co_varnames – a tuple containing names of every argument, both keyword-less and keyword-only used for this piece of code
    • co_posonlyargcount – number of positional only argument. Note that this applies only if you’re using the positional only feature:
def test(x, y, /, c):
   pass

So, if you’re trying to steal some other function’s type definition (eg. because you’re writing a decorator) please use the solution mentioned at the python/typing discussion. However, if you’re trying to write a decorator that will dynamically patch both normal routines and coroutines, then you’re in a pickle, because returning a normal Python def will cause your coroutines to turn them into normal routines and you won’t even see the cat.

However, if you’re trying to steal a coroutine, lets a FastAPI-based router, you’re at loss of luck. I’ve yet to invent a complete solution that allows you to do it. In order however for Python’s inspect module, you can attach the following:

def wrapped_function(x):
   return x

def inner(y):
   return wrapped_function(x):

import inspect
inner.__signature__ = inspect.signature(wrapped_function)

Python’s inspect module will the automatically determine to look at the wrapped method’s data instead. However, please note that not all libraries do support this, so copying extra data (such as __annotations__ or __doc__ might be necessary).

The solution I came up comes up to this:

if inspect.iscoroutinefunction(fun):
    async def async_inner(*args, **kwargs):
        try:
            return await fun(*args, **kwargs)
        except Exception as exc:
            if self.log_that:
                logger.exception('Exception found at %s', exc, exc_info=exc)
            if exc.__class__.__name__ == 'HTTPException' or not isinstance(exc, self.exc_types):
                raise
            raise HTTPException(
                status_code=self.code,
                detail=self.msg.format(exc=str(exc)),
            ) from exc

    async_inner.__signature__ = inspect.signature(fun)
a   async_inner.__doc__ = getattr(fun, '__doc__')
    return async_inner

So every time you need to steal someone’s type signature, please just add __signature__ as your property. Thank you!

The signature is added in order for automatic inspection tools (such as possible dependency injection a certain Web framework may opt to use) to be able to actually pull the relevant type signatures from the definition of the wrapped object. The functools wraps serves this purpose for normal callables, but since it is meant to deal with normal callables, and not async callables, is just not useful here and we have to do it manually. We also copy the __doc__ because some automated tool may generate the documentation that we wish to preserve just there and so that it gets picked up as well. And it’s done using getattr, in case our function has no docs at all.

This makes at least our unit test suite pass. Hope it helps. As usual, stay safe 🙂

Leave a comment

Your email address will not be published. Required fields are marked *