PEP:362
Title:Function Signature Object
Version:59031
Last-Modified:2007-11-16 20:20:22 -0800 (Fri, 16 Nov 2007)
Author:Brett Cannon <brett at python.org> Jiwon Seo <seojiwon at gmail.com>
Status:Draft
Type:Standards Track
Content-Type:text/x-rst
Created:21-Aug-2006
Python-Version:2.6
Post-History:05-Sep-2007

Contents

Abstract

Python has always supported powerful introspection capabilities, including that for functions and methods (for the rest of this PEP the word "function" refers to both functions and methods). Taking a function object, you can fully reconstruct the function's signature. Unfortunately it is a little unruly having to look at all the different attributes to pull together complete information for a function's signature.

This PEP proposes an object representation for function signatures. This should help facilitate introspection on functions for various uses. The introspection information contains all possible information about the parameters in a signature (including Python 3.0 features).

This object, though, is not meant to replace existing ways of introspection on a function's signature. The current solutions are there to make Python's execution work in an efficient manner. The proposed object representation is only meant to help make application code have an easier time to query a function on its signature.

Purpose

An object representation of a function's call signature should provide an easy way to introspect what a function expects as arguments. It does not need to be a "live" representation, though; the signature can be inferred once and stored without changes to the signature object representation affecting the function it represents (but this is an Open Issues).

Indirecation of signature introspection can also occur. If a decorator took a decorated function's signature object and set it on the decorating function then introspection could be redirected to what is actually expected instead of the typical *args, **kwargs signature of decorating functions.

Signature Object

The overall signature of an object is represented by the Signature object. This object is to store a Parameter object for each parameter in the signature. It is also to store any information about the function itself that is pertinent to the signature.

A Signature object has the following structure attributes:

Signature objects also have the following methods:

The Signature object is stored in the __signature__ attribute of a function. When it is to be created is discussed in Open Issues.

Parameter Object

A function's signature is made up of several parameters. Python's different kinds of parameters is quite large and rich and continues to grow. Parameter objects represent any possible parameter.

Originally the plan was to represent parameters using a list of parameter names on the Signature object along with various dicts keyed on parameter names to disseminate the various pieces of information one can know about a parameter. But the decision was made to incorporate all information about a parameter in a single object so as to make extending the information easier. This was originally put forth by Talin and the preferred form of Guido (as discussed at the 2006 Google Sprint).

The structure of the Parameter object is:

Implementation

An implementation can be found in Python's sandbox [1]. There is a function named signature() which returns the value stored on the __signature__ attribute if it exists, else it creates the Signature object for the function and sets __signature__. For methods this is stored directly on the im_func function object since that is what decorators work with.

Examples

Annotation Checker

def quack_check(fxn):
    """Decorator to verify arguments and return value quack as they should.

    Positional arguments.
    >>> @quack_check
    ... def one_arg(x:int): pass
    ...
    >>> one_arg(42)
    >>> one_arg('a')
    Traceback (most recent call last):
        ...
    TypeError: 'a' does not quack like a <type 'int'>


    *args
    >>> @quack_check
    ... def var_args(*args:int): pass
    ...
    >>> var_args(*[1,2,3])
    >>> var_args(*[1,'b',3])
    Traceback (most recent call last):
        ...
    TypeError: *args contains a a value that does not quack like a <type 'int'>

    **kwargs
    >>> @quack_check
    ... def var_kw_args(**kwargs:int): pass
    ...
    >>> var_kw_args(**{'a': 1})
    >>> var_kw_args(**{'a': 'A'})
    Traceback (most recent call last):
        ...
    TypeError: **kwargs contains a value that does not quack like a <type 'int'>

    Return annotations.
    >>> @quack_check
    ... def returned(x) -> int: return x
    ...
    >>> returned(42)
    42
    >>> returned('a')
    Traceback (most recent call last):
        ...
    TypeError: the return value 'a' does not quack like a <type 'int'>

    """
    # Get the signature; only needs to be calculated once.
    sig = Signature(fxn)
    def check(*args, **kwargs):
        # Find out the variable -> value bindings.
        bindings = sig.bind(*args, **kwargs)
        # Check *args for the proper quack.
        try:
            duck = sig.var_annotations[sig.var_args]
        except KeyError:
            pass
        else:
            # Check every value in *args.
            for value in bindings[sig.var_args]:
                if not isinstance(value, duck):
                    raise TypeError("*%s contains a a value that does not "
                                    "quack like a %r" %
                                    (sig.var_args, duck))
            # Remove it from the bindings so as to not check it again.
            del bindings[sig.var_args]
        # **kwargs.
        try:
            duck = sig.var_annotations[sig.var_kw_args]
        except (KeyError, AttributeError):
            pass
        else:
            # Check every value in **kwargs.
            for value in bindings[sig.var_kw_args].values():
                if not isinstance(value, duck):
                    raise TypeError("**%s contains a value that does not "
                                    "quack like a %r" %
                                    (sig.var_kw_args, duck))
            # Remove from bindings so as to not check again.
            del bindings[sig.var_kw_args]
        # For each remaining variable ...
        for var, value in bindings.items():
            # See if an annotation was set.
            try:
                duck = sig[var].annotation
            except AttributeError:
                continue
            # Check that the value quacks like it should.
            if not isinstance(value, duck):
                raise TypeError('%r does not quack like a %s' % (value, duck))
        else:
            # All the ducks quack fine; let the call proceed.
            returned = fxn(*args, **kwargs)
            # Check the return value.
            try:
                if not isinstance(returned, sig.return_annotation):
                    raise TypeError('the return value %r does not quack like '
                                    'a %r' % (returned,
                                        sig.return_annotation))
            except AttributeError:
                pass
            return returned
    # Full-featured version would set function metadata.
    return check

Open Issues

When to construct the Signature object?

The Signature object can either be created in an eager or lazy fashion. In the eager situation, the object can be created during creation of the function object. In the lazy situation, one would pass a function object to a function and that would generate the Signature object and store it to __signature__ if needed, and then return the value of __signature__.

Should Signature.bind return Parameter objects as keys?

Instead of returning a dict with keys consisting of the name of the parameters, would it be more useful to instead use Parameter objects? The name of the argument can easily be retrieved from the key (and the name would be used as the hash for a Parameter object).

Have var_args and _var_kw_args default to None?

It has been suggested by Fred Drake that these two attributes have a value of None instead of empty strings when they do not exist. The answer to this question will influence what the defaults are for other attributes as well.

Deprecate inspect.getargspec() and .formatargspec()?

Since the Signature object replicates the use of getargspec() from the inspect module it might make sense to deprecate it in 2.6. formatargspec() could also go if Signature objects gained a __str__ representation.

Issue with that is types such as int, when used as annotations, do not lend themselves for output (e.g., "<type 'int'>" is the string represenation for int). The repr representation of types would need to change in order to make this reasonable.

Have the objects be "live"?

Jim Jewett pointed out that Signature and Parameter objects could be "live". That would mean requesting information would be done on the fly instead of caching it on the objects. It would also allow for mutating the function if the Signature or Parameter objects were mutated.

References

[1]pep362 directory in Python's sandbox (http://svn.python.org/view/sandbox/trunk/pep362/)