__wrapped__ in Python decorators

This is an­oth­er of the new in­ter­est­ing things that Python 3 has. Time ago, it was some­how tricky to work with dec­o­rat­ed func­tion­s, be­cause the dec­o­ra­tor re­placed the orig­i­nal ob­jec­t, and its be­hav­iour be­came hard to reach.

So, for ex­am­ple, we all know that the fol­low­ing sen­tence:

def function():

It’s ac­tu­al­ly syn­tax sug­ar for:

def function():

function = decorator(function)

So, if for some rea­son, we need to work both with the orig­i­nal and dec­o­rat­ed ­func­tion­s, we re­quired tricks such as dif­fer­ent names, some­thing like:

def _function():

function = decorator(_function)

But in cur­rent Python, this should be no longer the case. As we can see from the code of the func­tools mod­ule [1], when dec­o­rat­ing an ob­jec­t, there is an at­tribute named __wrapped__ that holds the ref­er­ence to the orig­i­nal one.

So now if we use this, we can ac­cess it di­rect­ly with­out hav­ing to re­sort to the old quirk­s.


Let’s con­sid­er this ex­am­ple. Val­i­dat­ing pa­ram­e­ter types like this is far from a re­al im­ple­men­ta­tion, but rather some­thing for the sake of il­lus­tra­tion.

from functools import wraps

def validate_parameters(kwargs, annotations):
    """For a dictionary of kwargs, and another dictionary of annotations,
    mapping each argument name with its type, validate if all types on the
    keyword arguments match those described by the annotations.
    for arg, value in kwargs.items():
        expected_type = annotations[arg]
        if not isinstance(value, expected_type):
            raise TypeError(
                "{0}={1!r} is not of type {2!r}".format(
                    arg, value, expected_type)

def validate_function_parameters(function):
    def inner(**kwargs):
        annotations = function.__annotations__
        validate_parameters(kwargs, annotations)
        return function(**kwargs)
    return inner

def test_function(x: int, y:float):
    return x * y

test_function.__wrapped__(x=1, y=3)  # does not validate
test_function(x=1, y=3)  # validates and raises

The state­ment in the penul­ti­mate line works be­cause it’s ac­tu­al­ly in­vok­ing the o­rig­i­nal func­tion, with­out the dec­o­ra­tor ap­plied, where­as the last one fail­s be­cause it’s call­ing the func­tion al­ready dec­o­rat­ed.

Potential use cases

In gen­er­al this a nice fea­ture, for the rea­son that in a way en­ables ac­cess­ing both ob­ject­s, (it could be ar­gued that dec­o­rat­ing a func­tion caus­es some sort of tem­po­ral cou­pling).

Most im­por­tant­ly, it can be used in unit tests, whether is to test the o­rig­i­nal func­tion, or that the dec­o­ra­tor it­self is act­ing as it is sup­posed to ­do.

More­over, we could tests our own code, be­fore be­ing dec­o­rat­ed by oth­er li­braries be­ing used in the project (for ex­am­ple the func­tion of a tasks with­out the @cel­ery.­task dec­o­ra­tor ap­plied, or an event of the data­base of SQLAlche­my be­fore it was changed by the lis­ten­er event dec­o­ra­tor, etc.).

Just an­oth­er trick to keep in the tool­box.

[1] This attribute is documented, but I first found about it while reading at the code of CPython at https://github.com/python/cpython/blob/3405792b024e9c6b70c0d2355c55a23ac84e1e67/Lib/functools.py#L70