Python Decorator Proposals

See the next section for a categorization of different proposals; this section just provides concrete examples.


Decorator Poll (consider the poll now closed)

Here is an online poll where you can vote between a few different alternative syntaxes for python decorators: http://wiki.wxpython.org/index.cgi/PythonDecoratorsPoll

A more complete poll is currently running on comp.lang.python, with the unfortunate name of "Alternate decorator syntax decision", using the options on this WikiPage as the candidates. Please visit that thread and express your preference!


After the @decorator syntax was "accepted", lots of people threw up alarms and a huge series of threads started exploding on Python-dev. Here are the current alternatives that I could find that are being argued, with pros and cons.

I give two examples that might be common uses in the future. Classmethod declarations, and something like static typing (adapters), declaring what type parameters a function expects and returns.

A1. pie decorator syntax

@classmethod
def foo(arg1,arg2):
    ...

@accepts(int,int)
@returns(float)
def bar(low,high):
    ...

FWIW, here is Guido's jumble example in this syntax.

class C(object):

    @staticmethod
    @funcattrs(grammar="'@' dotted_name [ '(' [arglist] ')' ]",
               status="experimental", author="BDFL")
    def longMethodNameForEffect(longArgumentOne=None,
                                longArgumentTwo=42):
        """This method blah, blah.

        It supports the following arguments:
        - longArgumentOne -- a string giving ...
        - longArgumentTwo -- a number giving ...

        blah, blah.

        """
        raise NotYetImplemented

And here is an example taken from the current test_decorators.py. This exposes the problem of using two lines together with some meaning but without identation or vertical whitespace.

class TestDecorators(unittest.TestCase):

    ...

    def test_dotted(self):
        decorators = MiscDecorators()
        @decorators.author('Cleese')
        def foo(): return 42
        self.assertEqual(foo(), 42)
        self.assertEqual(foo.author, 'Cleese')

A2. pie decorator and space syntax

@ classmethod
def foo(arg1,arg2):
    ...

@ accepts(int,int)
@ returns(float)
def bar(low,high):
    ...

* 0 This is more readable for some people, less readable for others.

B. list-before-def syntax

[classmethod]
def foo(arg1,arg2):
    ...

[accepts(int,int), returns(float)]
def bar(low,high):
    ...

C1. list-after-def syntax

def foo(arg1,arg2) [classmethod]:
    ...

def bar(low,high) [accepts(int,int), returns(float)]:
    ...

I don't see why longs lists of decorators are an issue with this syntax. Consider the following example:

def foo(arg1, arg2) [
    complicated(manyArgs=1, notTooUgly='yes'),
    even_more_complicated(42)]:
    ...

That doesn't look particularly ugly to me.

---

It also isn't very long.

Here is an example Guido just sent to python-dev:

class C(object):

    def longMethodNameForEffect(longArgumentOne=None,
                                longArgumentTwo=42) [
        staticmethod,
        funcattrs(grammar="'@' dotted_name [ '(' [arglist] ')' ]",
                  status="experimental", author="BDFL")
        ]:
        """This method blah, blah.

        It supports the following arguments:
        - longArgumentOne -- a string giving ...
        - longArgumentTwo -- a number giving ...

        blah, blah.

        """
        raise NotYetImplemented

And he editorializes:

That's a total jumble of stuff ending with a smiley.  (True story: I
left out the colon when typing up this example and only noticed in
proofing.)

Problems with this form:

- it hides crucial information (e.g. that it is a static method)
  after the signature, where it is easily missed

- it's easy to miss the transition between a long argument list and a
  long decorator list

- it's cumbersome to cut and paste a decorator list for reuse, because
  it starts and ends in the middle of a line

Given that the whole point of adding decorator syntax is to move the
decorator from the end ("foo = staticmethod(foo)" after a 100-line
body) to the front, where it is more in-your-face, it should IMO be
moved all the way to the front.

C2. list-after-def syntax with a (pseudo-)keyword

def foo(arg1,arg2) using [classmethod]:
    ...

def bar(low,high) using [accepts(int,int), returns(float)]:
    ...

This combines C1 with a keyword; it general, it has all the advantages of either, so I will only list those that are unique to the combination.

FWIW, here is Guido's jumble example in this syntax.

class C(object):

    def longMethodNameForEffect(longArgumentOne=None,
                                longArgumentTwo=42) using
        [staticmethod,
         funcattrs(grammar="'@' dotted_name [ '(' [arglist] ')' ]",
                   status="experimental", author="BDFL")]:
        """This method blah, blah.

        It supports the following arguments:
        - longArgumentOne -- a string giving ...
        - longArgumentTwo -- a number giving ...

        blah, blah.

        """
        raise NotYetImplemented

Without the pseudo-keyword acting as line continuation it reads :

class C(object):

    def longMethodNameForEffect(longArgumentOne=None,
                                longArgumentTwo=42) using [
         staticmethod,
         funcattrs(grammar="'@' dotted_name [ '(' [arglist] ')' ]",
                   status="experimental", author="BDFL")]:
        """This method blah, blah.

        It supports the following arguments:
        - longArgumentOne -- a string giving ...
        - longArgumentTwo -- a number giving ...

        blah, blah.

        """
        raise NotYetImplemented

which feel is more consistent with the rest of python parsing-wise, without decreasing readability...

(See also J4 below, which moves the keyword, and uses "@" signs to make the decorators stand out more.)

C3. tuple-after-def syntax with a (pseudo-)keyword

def foo(arg1,arg2) using classmethod,:
    ...

def bar(low,high) using accepts(int,int), returns(float):
    ...
class C(object):

    def longMethodNameForEffect(longArgumentOne=None,
                                longArgumentTwo=42) using (
         staticmethod,
         funcattrs(grammar="'@' dotted_name [ '(' [arglist] ')' ]",
                   status="experimental", author="BDFL")):
        """This method blah, blah.

        It supports the following arguments:
        - longArgumentOne -- a string giving ...
        - longArgumentTwo -- a number giving ...

        blah, blah.

        """
        raise NotYetImplemented

Very similar to C2, but with those slight differences

The 1st drawback could be removed if one allows both tuple and single-element after the pseudo-keyword, trading consistency for readability and convenience.

C4. tuple-after-def syntax with a % operator

def foo(arg1,arg2) % classmethod:
    ...

def bar(low,high) % accepts(int,int), returns(float):
    ...
class C(object):

    def longMethodNameForEffect(longArgumentOne=None,
                                longArgumentTwo=42)             # make implicit linebreak possible here
         % (staticmethod,
            funcattrs(grammar="'@' dotted_name [ '(' [arglist] ')' ]",
                      status="experimental", author="BDFL")):
        """This method blah, blah.

        It supports the following arguments:
        - longArgumentOne -- a string giving ...
        - longArgumentTwo -- a number giving ...

        blah, blah.

        """
        raise NotYetImplemented

# this is also possible for consistency:

foo %= classmethod
bar %= (accepts(int,int), returns(float))

Very similar to C3, but with those slight differences

One more point: % could also be used in chained fashion:

bar = bar % accepts(int,int) % returns(float)

(making it similar to E2 below)

D1. list at top of function body syntax

def foo(arg1,arg2):
    [classmethod]
    ...

def bar(low,high):
    [accepts(int,int), returns(float)]
    ...

D2. 'dot'decorators at top of function body syntax

def bar(low,high):
    .accepts(int,int)
    .returns(float)
    """docstring"""
    pass

def longMethodNameForEffect(longArgumentOne=None,
                            longArgumentTwo=42):
    .staticmethod
    .funcattrs(grammar="'@' dotted_name [ '(' [arglist] ')' ]",
               status="experimental", author="BDFL")
    """
    asdfasdf
    """
    raise NotYetImplemented

Advantages/disadvantages of .decorators:

def func():
    .author = "Kevin Butler"
    pass

E1. pie decorator at top of function body syntax

def foo(arg1,arg2):
    @classmethod
    ...

def bar(low,high):
    @accepts(int,int)
    @returns(float)
    ...

E2. vbar decorator at top of function body syntax

def foo(arg1,arg2):
    |classmethod
    ...

def bar(low,high):
    |accepts(int,int)
    |returns(float)
    ...

def longMethodNameForEffect(longArgumentOne=None,
                            longArgumentTwo=42):
    |staticmethod
    |funcattrs(grammar="'@' dotted_name [ '(' [arglist] ')' ]",
               status="experimental", author="BDFL")
    """This method blah, blah.

    It supports the following arguments:
    - longArgumentOne -- a string giving ...
    - longArgumentTwo -- a number giving ...

    blah, blah.

    """

Restyled version:

def foo(arg1,arg2):
    | classmethod
    ...

def bar(low,high):
    | accepts(int,int)
    | returns(float)
    ...

def longMethodNameForEffect(longArgumentOne=None,
                            longArgumentTwo=42):
    | staticmethod
    | funcattrs(grammar="'@' dotted_name [ '(' [arglist] ')' ]",
               status="experimental", author="BDFL")
    """This method blah, blah.

    It supports the following arguments:
    - longArgumentOne -- a string giving ...
    - longArgumentTwo -- a number giving ...

    blah, blah.

    """

E3. vbar decorator after arg

def longMethodNameForEffect(longArgumentOne=None,
                            longArgumentTwo=42)
    |staticmethod
    |funcattrs(grammar="'@' dotted_name [ '(' [arglist] ')' ]",
               status="experimental", author="BDFL"):
    """This method blah, blah.

    It supports the following arguments:
    - longArgumentOne -- a string giving ...
    - longArgumentTwo -- a number giving ...

    blah, blah.

    """

def bar(low,high)
    |accepts(int,int)
    |returns(float):
    ...

def foo(arg1,arg2) | classmethod:
    ...

An alternative (inspired by a typing error I corrected in E2 in the Guido example) would be to put vbar decorator before the colon... Basically it has the same characteristic than E2, with the following slight differences:

E4. keyword decorator at top of function body syntax

def foo(arg1,arg2):
    using classmethod
    ...

def bar(low,high):
    using accepts(int,int)
    using returns(float)
    ...

def longMethodNameForEffect(longArgumentOne=None,
                            longArgumentTwo=42):
    using staticmethod
    using funcattrs(grammar="'@' dotted_name [ '(' [arglist] ')' ]",
               status="experimental", author="BDFL")
    """This method blah, blah.

    It supports the following arguments:
    - longArgumentOne -- a string giving ...
    - longArgumentTwo -- a number giving ...

    blah, blah.

    """

Similar to E1 and E2 but with keyword

F. inline syntax

def classmethod foo(arg1,arg2):
    ...

?

F2. inline syntax + new keyword (decodef for example)

decodef doo:
    classmethod
    funcattrs(...)

def [doo] foo(arg1,arg2):
    ...

G. as decorator

as classmethod
def foo(arg1,arg2):
    ...

?

H. pie decorator using a different character

For example, using the '|' character:

|classmethod
def foo(arg1,arg2):
    ...

|accepts(int,int)
|returns(float)
def bar(low,high):
    ...

Same pros and cons as @decorator, but additionally:

Restyled version:

| classmethod
def foo(arg1,arg2):
    ...

| accepts(int,int)
| returns(float)
def bar(low,high):
    ...

I. angle brackets decorator syntax

<classmethod>
def foo(arg1,arg2):
    ...

<accepts(int,int), returns(float)>
def bar(low,high):
    ...

J1. new keyword decorator syntax

decorate classmethod:
    def foo(arg1,arg2):
        ...

decorate accepts(int,int), returns(float):
    def bar(low,high):
        ...

Wouldn't be possible to allow both syntaxes?:

decorate classmethod def foo(arg1, arg2):
   ...

decorate classmethod:
   def foo(arg1, arg2):

Here's an example of run-away nesting (imagine trying to figure out which decorators apply to baz if these were non-trivial functions) (answer: but you can do the same with if, and it doesn't mean you have to... The main point of this proposal is to allow one level of indentaton, in which case it is clear, but having the drawback of the indentation. If you find three level of indentation unclear, why write code that way? No other proposition has this "grouping" capacity anyway...) (answer answer: The original point was only that they can all be abused, but that this syntax can be abused in a way that is unique, simply because of the grouping capability.):

 decorate static, synchronized:
   decorate returns(None):
     decorate accepts(int):
       def foo(a):
         pass
     decorate accepts(int, int):
       def bar(a, b):
         pass
   decorate accepts(), returns(int):
     def baz():
       return 0

J2. expand the def suite

decorate:
    classmethod
def foo(arg1,arg2):
    ...

using:
    """
    It is now clear the docstring will 
    survive the decoration process 
    """
    accepts(int,int)
    returns(float)
def bar(low,high):
    ...

An extensive paper has been written by Robert Brewer:

  • - Requires new keyword
  • (- No one can agree on the right keyword to use.) The consensus is "using".
  • - Keyword chosen may conflict with how keyword is used in other languages (like "using" in C# or "extends" in Java and other languages).
  • - Keyword may be confused with other keywords in current and future versions of Python (like "with" in Python3.0, which is very similar to "using"). This would suggest choosing a less generic keyword like "decorate", "meta", or "predef".

  • (- Indented decorators put them visually in line with the function name. When indented below the function declaration line this isn't a big deal, but right above the function declaration it can be a problem.) This is so minor that it ought to be ignored.
  • (- Much more typing required than A1 pie syntax the more decorators you have.) But less shifting. (You also have to do more indentation checking.) Any decent editor does this with one keystroke. Try typing the two:

    using:
        classmethod
        accepts(int,int)
        returns(float)
    def bar(low,high):
        ...
    
    vs.
    
    @classmethod
    @accepts(int,int)
    @returns(float)
    def bar(low,high):
        ...
  • - Overkill for the simple case like classmethod (not that much, since "@" can be replaced by "using:" without new line). (Two extra lines still feels like overkill to me; a line with an internal colon is less overkill, but has its own style problems.)

  • - Many people felt it was wrong use of an identation suite. (Someone please explain. It looks just like any other pair of blocks in Python, such as if/else or try/finally.) In current python, an indent either adds a new namespace (this doesn't, it modifies the parent namespace where the def itself appears) or changes flow control. I do think it changes flow control as much as an if or a loop, but others felt strongly that it wasn't the same, and that the equivalence of decorators within a decorate block was different from the equivalence of statements in a function block. For a sample of the response, search the python-dev archives around March 2004. Private email was more adamant.

  • - Has the same problem as option A: in that the decorate block implicitly affects the following def. This does not occur elsewhere in Python. -- Or more precisely, it doesn't occur with an indented block syntax; parentheses can affect execution order. The proposed with: block would do the same thing. No, the with block wouldn't affect blocks that come after the with block like this does.

  • - Same problem as A1 and other proposals, in what order are the decorators applied? C1 is just about the only proposal that doesn't suffer from this problem.
  • - Where will the docstring go? Guido wants the docstring before the function. (He said it would be nice if it went there, that's all.) Does it have to be at the beginning of the decorate: indentation block? Or would it have to be at the end? Or where? What if the function has no decorators, would you have to still use "decorate" followed by an indented docstring? (Not before Python 3000, if ever.)

  • - Technical problems with the current grammar parser if a suite *starts* with an optional part. (Ending with an optional part, such as "else:" is OK, but starting with one is not.) (Michael Sparks has experimented with the grammar and based on his results it appears *not* an issue.)

  • The implementation referenced above does NOT allow a generic suite - but only allows decorators to be listed. The semantics wrt to ordering remains the same as in 2.4a2

    (Instead of the word "decorate" it is possible to use some other less lengthy keyword. And even DEF taken uppercase.)

    J3. two part def suite

    def qux:
        """docstring could be here or below. Two strings probable"""
        decor1
        decor2
    from arg1, arg2:
        ...
    
    def quux staticmethod from (arg1, arg2):  return arg1 + arg2 # possible one-liner (see K)
    
    # which probably is not much needed because of shorter legacy:
    quux = staticmethod(lambda arg1, arg2: arg1 + arg2)
    
    # unstylish, but still possible:
    def quuux: decor1; decor2
    from x, y: return x + y

    Same as J2 but has advantages:

    J4 two part def suite with "@" decorators

    (Note: the missing colon is *intentional*. See J5 for version with colon.)

       def func(self, arg1)
           @staticmethod
           @grammarrule('statement : expression')
           @version("Added in 2.4")
       as:
           """Docstring could be here, or in decorator part above"""
           # body goes here

    Note that this differs from J5 only by the colon. J5 in turn differs from J2 in whether the function signature stays with the function body, or moves to the top suite. (And in whether the redundant "@" is added for extra readability.)

    if (test):   # They match the suite, rather than the statements
        stmt1
        stmt2

    J5 two part def suite with "@" decorators and colon

    While the missing colon of J4 was intentional, there are good arguments both ways. Since we're listing options, I include this variant of J4.

       def func(self, arg1):
           @staticmethod
           @grammarrule('statement : expression')
           @version("Added in 2.4")
       as:
           """Docstring could be here, or in decorator part above"""
           # body goes here

    Note that J5 differs from J2 only in whether the function signature stays with the function body, or moves to the top suite. (And in whether the redundant "@" is added for extra readability.)

    K. partitioned syntax syntax

    def classmethod foo(arg1,arg2):
        ...

    L. Keyword other than as and with before def

    using classmethod def foo(arg1,arg2):
        ...
    
    using accepts(int,int)
    using returns(float)
    def bar(low,high):
        ...
    
    other possible keyword:
    
    predef classmethod
    def foo(arg1,arg2):
        ...
    
    predef accepts(int,int)
    predef returns(float)
    def bar(low,high):
        ...

    M. Making def an expression / letting it return a value

    deco1(deco2(deco3(
        def func(arg1, arg2):
            pass
    )))
    
        deco1(
    
        def method(self, arg):
            pass
        )

    Decorator Syntax Breakdown

    Here's a breakdown of some of the different decisions that have to be made in deciding on a decorator syntax. This is an attempt to consolidate some of the common points from the various examples above.

    Indicator

    Proposals differ on how some sort of indicator of "decoratorhood" is use. These include:

    Location

    Proposals also differ on exactly where the decorator declaration should be made. These include:

    List Notation

    Decorator syntax, as described in the PEP, must support the application of multiple decorators. Some proposals on how to support this include:

    Indentation

    Proposals also differ on whether or not decorators should introduce a new block. These include:

    Order of Decorators

    Should decorators be applied in textual order, or in reverse order (as if they were in nested parentheses around the def).

    It seems to have been settled that decorators will be applied starting with the one closest to the "def" statement. (This means in-order vs reverse will depend on the eventual location.)

    Allowable Decorators

    The eventual decision was that anything should be allowed, but it *will* be called with a single argument (the function object) before the function's name is bound in the enclosing namespace.

    Should None be allowed and just not called? This would allow statements in the decorator suite, and might be useful, but ... it wasn't deemed useful enough for a special case, at least in 2.4

    Meaning of decorators

    @deco1
    @deco2(darg1, darg2)
    @deco3
    def func(arg1, arg2):
        pass

    is equivalent to

    __tempd1 = deco1
    __tempd2 = deco2(darg1, darg2)
    __tempd3 = deco3
    __tempf = lambda arg1, arg2: pass
    func = tempd1(tempd2(tempd3(tempf)))

    with the following exceptions:

    Thinking ahead to Python 3 ?

    Christopher King makes the point that we are trying to do too much with decorators: declare class/static methods, describe function metadata, and mangle functions. It might be best to think about what is best for each separately.

    How might fully loaded functions look in the future?

    Christopher King's example:

    def classmethod foo(self,a,b,c):
        """Returns a+b*c."""
        {accepts: (int,int,int), author: 'Chris King'}
    
        return a+b*c

    Another possible example (keyword support for staticmethod & classmethod, visual basic-like typing using the "as" keyword for adapters, "with" code blocks):

    def classmethod foo(a as int, b as int, c as list) as list:
        """Returns a+b*c."""
    
        listcopy = []
        with synchronized(lock):
            listcopy[] = c[]
    
        return a+b*listcopy

    Here it is with the @ symbol:

     @author('Chris King')
     @accepts(int,int,list)
     @classmethod
     def foo(self,a,b,c):
         """Returns a+b*c."""
    
          return a+b*c

    PythonDecoratorProposals (last edited 2008-11-15 14:00:26 by localhost)

    Unable to edit the page? See the FrontPage for instructions.

    Follow Lee on X/Twitter - Father, Husband, Serial builder creating AI, crypto, games & web tools. We are friends :) AI Will Come To Life!

    Check out: eBank.nz (Art Generator) | Netwrck.com (AI Tools) | Text-Generator.io (AI API) | BitBank.nz (Crypto AI) | ReadingTime (Kids Reading) | RewordGame | BigMultiplayerChess | WebFiddle | How.nz | Helix AI Assistant